From bc15e7a7103cb0e49d5ea6e7eb01919feec2e1ad Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 23 Jun 2020 11:56:56 +0200 Subject: [PATCH 1/6] feat(scaffolder): cookiecutter templater wip Co-authored-by: Ben Lambert --- plugins/scaffolder-backend/package.json | 1 + .../hooks/post_gen_project.sh | 17 ++++++---------- .../hooks/pre_gen_project.sh | 9 --------- .../package.json | 2 +- plugins/scaffolder-backend/scripts/Dockerfile | 11 ++++++++++ .../src/scaffolder/templater/cookiecutter.ts | 20 +++++++++++++++++-- .../scaffolder-backend/src/service/router.ts | 9 ++++++--- yarn.lock | 7 +++++++ 8 files changed, 50 insertions(+), 26 deletions(-) delete mode 100644 plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/pre_gen_project.sh create mode 100644 plugins/scaffolder-backend/scripts/Dockerfile diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 0edf7a8fe9..e85a65e744 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -37,6 +37,7 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.9", + "@types/dockerode": "^2.5.32", "@types/fs-extra": "^9.0.1", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/post_gen_project.sh b/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/post_gen_project.sh index 033a102fed..75c35f2034 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/post_gen_project.sh +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/post_gen_project.sh @@ -1,12 +1,7 @@ -#!/bin/bash +#!/bin/sh -# package name is "__component_id__" so that yarn doesn't throw an error -# about invalid characters when running yarn commands. here we replace it with the actual name -sed -i -e "s/__component_id__/{{ cookiecutter.component_id }}/g" package.json - -# node_modules was moved out of the template folder, during the pre_gen hook, -# to avoid cookie_cutter from copying all of them. time to move it back -mv ../../node_modules.tmp ../../\{\{cookiecutter.component_id\}\}/node_modules 2>/dev/null ||: - -# move back the build directory that was moved out in the pre_gen hook (if it exists) -mv ../../build.tmp ../../\{\{cookiecutter.component_id\}\}/build 2>/dev/null ||: +# Move all template files to the root folder +mv ./* ../ +cd .. +rm -rf {{cookiecutter.component_id}} +# # # # # # # # diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/pre_gen_project.sh b/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/pre_gen_project.sh deleted file mode 100644 index 142fda9ad9..0000000000 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/hooks/pre_gen_project.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# no way to ignore files in cookiecutter, so move node_modules out while building -# to avoid cookiecutter from copying all of them -mv ../../\{\{cookiecutter.component_id\}\}/node_modules ../../node_modules.tmp 2>/dev/null ||: - -# cookicutter really doesn't like the next.js build directory, so if the app has -# been built from inside the template folder, that folders needs to be moved out as well -mv ../../\{\{cookiecutter.component_id\}\}/build ../../build.tmp 2>/dev/null ||: diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/package.json b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/package.json index a0ae26e267..6ae3e63a17 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/package.json +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/package.json @@ -1,5 +1,5 @@ { - "name": "__component_id__", + "name": "{{ cookiecutter.component_id }}", "version": "0.0.0", "description": "{{ cookiecutter.description }}", "license": "UNLICENSED", diff --git a/plugins/scaffolder-backend/scripts/Dockerfile b/plugins/scaffolder-backend/scripts/Dockerfile new file mode 100644 index 0000000000..b33c7cbe64 --- /dev/null +++ b/plugins/scaffolder-backend/scripts/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine:3.7 + +RUN apk add --update \ + git \ + python \ + python-dev \ + py-pip \ + g++ && \ + pip install cookiecutter && \ + apk del g++ py-pip python-dev && \ + rm -rf /var/cache/apk/* diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts index ee5265fde8..cabb1d4f1e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts @@ -16,9 +16,18 @@ import { TemplaterBase, TemplaterRunOptions } from '.'; * limitations under the License. */ import fs from 'fs-extra'; +import Docker from 'dockerode'; export class CookieCutter implements TemplaterBase { + private docker:Docker; + constructor() { + this.docker = new Docker(); + } + public async run(options: TemplaterRunOptions): Promise { + + + // first we need to make cookiecutter.json in the directory provided with the input values. const cookieInfo = { _copy_without_render: ['.github/workflows/*'], @@ -26,7 +35,14 @@ export class CookieCutter implements TemplaterBase { }; await fs.writeJSON(`${options.directory}/cookiecutter.json`, cookieInfo); - return ''; - // run cookie cutter with new json + const realTemplatePath = await fs.promises.realpath(options.directory); + const outDir = realTemplatePath + '/result'; + + + await this.docker.run('backstage/cookiecutter', ['cookiecutter', '--no-input', '-o', '/result', '/template', '--verbose'], process.stdout, {Volumes: { '/result': {}, '/template': {}}, HostConfig: { + Binds: [`${outDir}:/result`, `${realTemplatePath}:/template`], + }}); + + return outDir; } } diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index c9b92494fd..e71593b6ba 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -64,12 +64,15 @@ export async function createRouter( const preparer = preparers.get(mockEntity); // Run the preparer for the mock entity to produce a temporary directory with template in - const path = await preparer.prepare(mockEntity); + const skeletonPath = await preparer.prepare(mockEntity); // Run the templater on the mock directory with values from the post body - await templater.run({ directory: path, values: { component_id: 'test' } }); + const templatedPath = await templater.run({ + directory: skeletonPath, + values: { component_id: 'test', description: "Something for now" }, + }); - console.warn(path); + console.warn(templatedPath); }); const app = express(); diff --git a/yarn.lock b/yarn.lock index fd351efe36..88a9861af6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3520,6 +3520,13 @@ resolved "https://registry.npmjs.org/@types/diff/-/diff-4.0.2.tgz#2e9bb89f9acc3ab0108f0f3dc4dbdcf2fff8a99c" integrity sha512-mIenTfsIe586/yzsyfql69KRnA75S8SVXQbTLpDejRrjH0QSJcpu3AUOi/Vjnt9IOsXKxPhJfGpQUNMueIU1fQ== +"@types/dockerode@^2.5.32": + version "2.5.32" + resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-2.5.32.tgz#52d3628f605f8ea65202541c59a8a6dd166384fd" + integrity sha512-TfaGOoOHxsjkWRj2sPoQ3FLmTC5mVMhZ4kzZy13U7mjtIDoloE4e7AMj5jPLbffWB6Csy5DF5e0lC9M+tnKz/A== + dependencies: + "@types/node" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" From 12db9bc694dba19bf992e91632f8283ac1738ca3 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 23 Jun 2020 14:17:28 +0200 Subject: [PATCH 2/6] chore: add tests, merge cookiecutter.json Co-authored-by: Ben Lambert --- plugins/scaffolder-backend/package.json | 1 + .../react-ssr-template/cookiecutter.json | 3 + .../scaffolder/templater/cookiecutter.test.ts | 119 ++++++++++++++++++ .../src/scaffolder/templater/cookiecutter.ts | 42 +++++-- 4 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 plugins/scaffolder-backend/sample-templates/react-ssr-template/cookiecutter.json create mode 100644 plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index e85a65e744..b356f026e5 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -23,6 +23,7 @@ "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.9", "@backstage/catalog-model": "^0.1.1-alpha.9", + "@backstage/config": "^0.1.1-alpha.9", "@types/express": "^4.17.6", "compression": "^1.7.4", "cors": "^2.8.5", diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/cookiecutter.json b/plugins/scaffolder-backend/sample-templates/react-ssr-template/cookiecutter.json new file mode 100644 index 0000000000..bc6e4b5347 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/cookiecutter.json @@ -0,0 +1,3 @@ +{ + "_copy_without_render": [".github/workflows/*"] +} diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts new file mode 100644 index 0000000000..8362a759bf --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { CookieCutter } from './cookiecutter'; +import fs from 'fs-extra'; +import os from 'os'; + +const mockDocker = { run: jest.fn() }; +jest.mock( + 'dockerode', + () => + class { + constructor() { + return mockDocker; + } + }, +); + +describe('CookieCutter Templater', () => { + const cookie = new CookieCutter(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should write a cookiecutter.json file with the values from the entitiy', async () => { + const tempdir = os.tmpdir(); + + const values = { + component_id: 'test', + description: 'description', + }; + + await cookie.run({ directory: tempdir, values }); + + const cookieCutterJson = await fs.readJSON(`${tempdir}/cookiecutter.json`); + + expect(cookieCutterJson).toEqual(expect.objectContaining(values)); + }); + + it('should merge any value that is in the cookiecutter.json path already', async () => { + const tempdir = os.tmpdir(); + const existingJson = { + _copy_without_render: ['./github/workflows/*'], + }; + await fs.writeJSON(`${tempdir}/cookiecutter.json`, existingJson); + + const values = { + component_id: 'hello', + description: 'im something cool', + }; + + await cookie.run({ directory: tempdir, values }); + + const cookieCutterJson = await fs.readJSON(`${tempdir}/cookiecutter.json`); + + expect(cookieCutterJson).toEqual({ ...existingJson, ...values }); + }); + + it('should run the correct docker container with the correct bindings for the volumes', async () => { + const tempdir = os.tmpdir(); + + const values = { + component_id: 'test', + description: 'description', + }; + + await cookie.run({ directory: tempdir, values }); + + const realpath = await fs.realpath(tempdir); + + // TODO(blam): This might change when we publish our own cookiecutter image + // to @backstage/cookiecutter in docker hub. + expect(mockDocker.run).toHaveBeenCalledWith( + 'backstage/cookiecutter', + ['cookiecutter', '--no-input', '-o', '/result', '/template', '--verbose'], + process.stdout, + expect.objectContaining({ + HostConfig: { + Binds: expect.arrayContaining([ + `${realpath}:/template`, + `${realpath}/result:/result`, + ]), + }, + Volumes: { + '/template': {}, + '/result': {}, + }, + }), + ); + }); + + it('should return the result path to the end templated folder', async () => { + const tempdir = os.tmpdir(); + + const values = { + component_id: 'test', + description: 'description', + }; + + const path = await cookie.run({ directory: tempdir, values }); + + const realpath = await fs.realpath(tempdir); + + expect(path).toBe(`${realpath}/result`); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts index cabb1d4f1e..e927108dc4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts @@ -17,32 +17,52 @@ import { TemplaterBase, TemplaterRunOptions } from '.'; */ import fs from 'fs-extra'; import Docker from 'dockerode'; +import { JsonValue } from '@backstage/config'; export class CookieCutter implements TemplaterBase { - private docker:Docker; + private docker: Docker; constructor() { this.docker = new Docker(); } + private async fetchTemplateCookieCutter( + directory: string, + ): Promise> { + try { + return await fs.readJSON(`${directory}/cookiecutter.json`); + } catch (ex) { + return {}; + } + } + public async run(options: TemplaterRunOptions): Promise { - - - - // first we need to make cookiecutter.json in the directory provided with the input values. + // First lets grab the default cookiecutter.json file + const cookieCutterJson = await this.fetchTemplateCookieCutter( + options.directory, + ); + const cookieInfo = { - _copy_without_render: ['.github/workflows/*'], + ...cookieCutterJson, ...options.values, }; await fs.writeJSON(`${options.directory}/cookiecutter.json`, cookieInfo); + const realTemplatePath = await fs.promises.realpath(options.directory); - const outDir = realTemplatePath + '/result'; + const outDir = `${realTemplatePath}/result`; + await this.docker.run( + 'backstage/cookiecutter', + ['cookiecutter', '--no-input', '-o', '/result', '/template', '--verbose'], + process.stdout, + { + Volumes: { '/result': {}, '/template': {} }, + HostConfig: { + Binds: [`${outDir}:/result`, `${realTemplatePath}:/template`], + }, + }, + ); - await this.docker.run('backstage/cookiecutter', ['cookiecutter', '--no-input', '-o', '/result', '/template', '--verbose'], process.stdout, {Volumes: { '/result': {}, '/template': {}}, HostConfig: { - Binds: [`${outDir}:/result`, `${realTemplatePath}:/template`], - }}); - return outDir; } } From e59cac61fd44d153c6940963b85ac50c3a3021e6 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 23 Jun 2020 15:30:38 +0200 Subject: [PATCH 3/6] feat(scaffolder): error handling, log propagation Co-authored-by: Ben Lambert --- .../scaffolder/templater/cookiecutter.test.ts | 70 +++++++++++++++++-- .../src/scaffolder/templater/cookiecutter.ts | 22 ++++-- .../src/scaffolder/templater/index.ts | 3 + .../scaffolder-backend/src/service/router.ts | 2 +- 4 files changed, 88 insertions(+), 9 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts index 8362a759bf..bddf5a7b25 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts @@ -16,8 +16,11 @@ import { CookieCutter } from './cookiecutter'; import fs from 'fs-extra'; import os from 'os'; +import Stream, { PassThrough } from 'stream'; -const mockDocker = { run: jest.fn() }; +const mockDocker = { + run: jest.fn(() => [{ Error: null, StatusCode: 0 }]), +}; jest.mock( 'dockerode', () => @@ -32,7 +35,7 @@ describe('CookieCutter Templater', () => { const cookie = new CookieCutter(); beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should write a cookiecutter.json file with the values from the entitiy', async () => { @@ -85,8 +88,8 @@ describe('CookieCutter Templater', () => { // to @backstage/cookiecutter in docker hub. expect(mockDocker.run).toHaveBeenCalledWith( 'backstage/cookiecutter', - ['cookiecutter', '--no-input', '-o', '/result', '/template', '--verbose'], - process.stdout, + ['cookiecutter', '--no-input', '-o', '/result', '/template'], + expect.any(Stream), expect.objectContaining({ HostConfig: { Binds: expect.arrayContaining([ @@ -116,4 +119,63 @@ describe('CookieCutter Templater', () => { expect(path).toBe(`${realpath}/result`); }); + + it('throws a correct error if the templating fails in docker', async () => { + mockDocker.run.mockResolvedValueOnce([ + { + Error: new Error('Something went wrong with docker'), + StatusCode: 0, + }, + ]); + + const tempdir = os.tmpdir(); + + const values = { + component_id: 'test', + description: 'description', + }; + + await expect(cookie.run({ directory: tempdir, values })).rejects.toThrow( + /Something went wrong with docker/, + ); + }); + + it('uses the passed stream as a log stream', async () => { + const logStream = new PassThrough(); + const tempdir = os.tmpdir(); + + const values = { + component_id: 'test', + description: 'description', + }; + + await cookie.run({ directory: tempdir, values, logStream }); + + expect(mockDocker.run).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + logStream, + expect.any(Object), + ); + }); + + it('throws a correct error if the container returns a non-zero exit code', async () => { + mockDocker.run.mockResolvedValueOnce([ + { + Error: null, + StatusCode: 1, + }, + ]); + + const tempdir = os.tmpdir(); + + const values = { + component_id: 'test', + description: 'description', + }; + + await expect(cookie.run({ directory: tempdir, values })).rejects.toThrow( + /Docker container returned a non-zero exit code \(1\)/, + ); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts index e927108dc4..b612900123 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts @@ -18,7 +18,7 @@ import { TemplaterBase, TemplaterRunOptions } from '.'; import fs from 'fs-extra'; import Docker from 'dockerode'; import { JsonValue } from '@backstage/config'; - +import { PassThrough } from 'stream'; export class CookieCutter implements TemplaterBase { private docker: Docker; constructor() { @@ -51,10 +51,12 @@ export class CookieCutter implements TemplaterBase { const realTemplatePath = await fs.promises.realpath(options.directory); const outDir = `${realTemplatePath}/result`; - await this.docker.run( + const [ + { Error: dockerError, StatusCode: containerStatusCode }, + ] = await this.docker.run( 'backstage/cookiecutter', - ['cookiecutter', '--no-input', '-o', '/result', '/template', '--verbose'], - process.stdout, + ['cookiecutter', '--no-input', '-o', '/result', '/template'], + options.logStream ?? new PassThrough(), { Volumes: { '/result': {}, '/template': {} }, HostConfig: { @@ -63,6 +65,18 @@ export class CookieCutter implements TemplaterBase { }, ); + if (dockerError) { + throw new Error( + `Docker failed to run with the following error message: ${dockerError}`, + ); + } + + if (containerStatusCode !== 0) { + throw new Error( + `Docker container returned a non-zero exit code (${containerStatusCode})`, + ); + } + return outDir; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/index.ts b/plugins/scaffolder-backend/src/scaffolder/templater/index.ts index 80570a1c48..0c255354fe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/index.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import type { Writable } from 'stream'; + export interface RequiredTemplateValues { component_id: string; } @@ -21,6 +23,7 @@ export interface RequiredTemplateValues { export interface TemplaterRunOptions { directory: string; values: RequiredTemplateValues & object; + logStream?: Writable; } export abstract class TemplaterBase { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index e71593b6ba..6636fc4b68 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -69,7 +69,7 @@ export async function createRouter( // Run the templater on the mock directory with values from the post body const templatedPath = await templater.run({ directory: skeletonPath, - values: { component_id: 'test', description: "Something for now" }, + values: { component_id: 'test' }, }); console.warn(templatedPath); From 0fe8e594f238702447f99c8d9654dd712cd33fbc Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 23 Jun 2020 18:07:03 +0200 Subject: [PATCH 4/6] refactor(scaffolder): Moving out the dockerish logic into something that is easy to consume for other templaters --- .../react-ssr-template/template.yaml | 1 - .../scaffolder/templater/cookiecutter.test.ts | 115 ++++----------- .../src/scaffolder/templater/cookiecutter.ts | 48 ++----- .../src/scaffolder/templater/helpers.test.ts | 131 ++++++++++++++++++ .../src/scaffolder/templater/helpers.ts | 75 ++++++++++ .../src/scaffolder/templater/templaters.ts | 38 +++++ 6 files changed, 286 insertions(+), 122 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/templater/helpers.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/templater/helpers.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/templater/templaters.ts diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml index 17f9ebbf7d..6834f0d1b6 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml @@ -7,4 +7,3 @@ metadata: spec: type: cookiecutter path: '.' - diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts index bddf5a7b25..bba3705a41 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts @@ -13,27 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +jest.mock('./helpers', () => ({ runDockerContainer: jest.fn() })); + import { CookieCutter } from './cookiecutter'; import fs from 'fs-extra'; import os from 'os'; -import Stream, { PassThrough } from 'stream'; - -const mockDocker = { - run: jest.fn(() => [{ Error: null, StatusCode: 0 }]), -}; -jest.mock( - 'dockerode', - () => - class { - constructor() { - return mockDocker; - } - }, -); +import { RunDockerContainerOptions } from './helpers'; +import { PassThrough } from 'stream'; describe('CookieCutter Templater', () => { const cookie = new CookieCutter(); + const { + runDockerContainer, + }: { + runDockerContainer: jest.Mock; + } = require('./helpers'); + beforeEach(() => { jest.clearAllMocks(); }); @@ -82,29 +78,14 @@ describe('CookieCutter Templater', () => { await cookie.run({ directory: tempdir, values }); - const realpath = await fs.realpath(tempdir); - - // TODO(blam): This might change when we publish our own cookiecutter image - // to @backstage/cookiecutter in docker hub. - expect(mockDocker.run).toHaveBeenCalledWith( - 'backstage/cookiecutter', - ['cookiecutter', '--no-input', '-o', '/result', '/template'], - expect.any(Stream), - expect.objectContaining({ - HostConfig: { - Binds: expect.arrayContaining([ - `${realpath}:/template`, - `${realpath}/result:/result`, - ]), - }, - Volumes: { - '/template': {}, - '/result': {}, - }, - }), - ); + expect(runDockerContainer).toHaveBeenCalledWith({ + imageName: 'backstage/cookiecutter', + args: ['cookiecutter', '--no-input', '-o', '/result', '/template'], + templateDir: tempdir, + resultDir: `${tempdir}/result`, + logStream: undefined, + }); }); - it('should return the result path to the end templated folder', async () => { const tempdir = os.tmpdir(); @@ -115,18 +96,11 @@ describe('CookieCutter Templater', () => { const path = await cookie.run({ directory: tempdir, values }); - const realpath = await fs.realpath(tempdir); - - expect(path).toBe(`${realpath}/result`); + expect(path).toBe(`${tempdir}/result`); }); - it('throws a correct error if the templating fails in docker', async () => { - mockDocker.run.mockResolvedValueOnce([ - { - Error: new Error('Something went wrong with docker'), - StatusCode: 0, - }, - ]); + it('should pass through the streamer to the run docker helper', async () => { + const stream = new PassThrough(); const tempdir = os.tmpdir(); @@ -135,47 +109,14 @@ describe('CookieCutter Templater', () => { description: 'description', }; - await expect(cookie.run({ directory: tempdir, values })).rejects.toThrow( - /Something went wrong with docker/, - ); - }); + await cookie.run({ directory: tempdir, values, logStream: stream }); - it('uses the passed stream as a log stream', async () => { - const logStream = new PassThrough(); - const tempdir = os.tmpdir(); - - const values = { - component_id: 'test', - description: 'description', - }; - - await cookie.run({ directory: tempdir, values, logStream }); - - expect(mockDocker.run).toHaveBeenCalledWith( - expect.any(String), - expect.any(Array), - logStream, - expect.any(Object), - ); - }); - - it('throws a correct error if the container returns a non-zero exit code', async () => { - mockDocker.run.mockResolvedValueOnce([ - { - Error: null, - StatusCode: 1, - }, - ]); - - const tempdir = os.tmpdir(); - - const values = { - component_id: 'test', - description: 'description', - }; - - await expect(cookie.run({ directory: tempdir, values })).rejects.toThrow( - /Docker container returned a non-zero exit code \(1\)/, - ); + expect(runDockerContainer).toHaveBeenCalledWith({ + imageName: 'backstage/cookiecutter', + args: ['cookiecutter', '--no-input', '-o', '/result', '/template'], + templateDir: tempdir, + resultDir: `${tempdir}/result`, + logStream: stream, + }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts index b612900123..70a0cc28af 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts @@ -16,15 +16,10 @@ import { TemplaterBase, TemplaterRunOptions } from '.'; * limitations under the License. */ import fs from 'fs-extra'; -import Docker from 'dockerode'; import { JsonValue } from '@backstage/config'; -import { PassThrough } from 'stream'; -export class CookieCutter implements TemplaterBase { - private docker: Docker; - constructor() { - this.docker = new Docker(); - } +import { runDockerContainer } from './helpers'; +export class CookieCutter implements TemplaterBase { private async fetchTemplateCookieCutter( directory: string, ): Promise> { @@ -48,35 +43,20 @@ export class CookieCutter implements TemplaterBase { await fs.writeJSON(`${options.directory}/cookiecutter.json`, cookieInfo); - const realTemplatePath = await fs.promises.realpath(options.directory); - const outDir = `${realTemplatePath}/result`; + const templateDir = options.directory; - const [ - { Error: dockerError, StatusCode: containerStatusCode }, - ] = await this.docker.run( - 'backstage/cookiecutter', - ['cookiecutter', '--no-input', '-o', '/result', '/template'], - options.logStream ?? new PassThrough(), - { - Volumes: { '/result': {}, '/template': {} }, - HostConfig: { - Binds: [`${outDir}:/result`, `${realTemplatePath}:/template`], - }, - }, - ); + // TODO(blam): This should be an entirely different directory on the host machine + // not in the template directory + const resultDir = `${templateDir}/result`; - if (dockerError) { - throw new Error( - `Docker failed to run with the following error message: ${dockerError}`, - ); - } + await runDockerContainer({ + imageName: 'backstage/cookiecutter', + args: ['cookiecutter', '--no-input', '-o', '/result', '/template'], + templateDir, + resultDir, + logStream: options.logStream, + }); - if (containerStatusCode !== 0) { - throw new Error( - `Docker container returned a non-zero exit code (${containerStatusCode})`, - ); - } - - return outDir; + return resultDir; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/templater/helpers.test.ts new file mode 100644 index 0000000000..a5ac00165e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/templater/helpers.test.ts @@ -0,0 +1,131 @@ +/* + * 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 Stream, { PassThrough } from 'stream'; +import os from 'os'; +import fs from 'fs'; + +const mockDocker = { + run: jest.fn(() => [{ Error: null, StatusCode: 0 }]), +}; + +jest.mock( + 'dockerode', + () => + class { + constructor() { + return mockDocker; + } + }, +); + +import { runDockerContainer } from './helpers'; + +describe('helpers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('runDockerContainer', () => { + const imageName = 'blam/github:ben'; + const args = ['bash', '-c', 'echo lol']; + const templateDir = os.tmpdir(); + const resultDir = os.tmpdir(); + + it('should call the dockerClient run command with the correct arguments passed through', async () => { + await runDockerContainer({ + imageName, + args, + templateDir, + resultDir, + }); + + expect(mockDocker.run).toHaveBeenCalledWith( + imageName, + args, + expect.any(Stream), + expect.objectContaining({ + HostConfig: { + Binds: expect.arrayContaining([ + `${await fs.promises.realpath(templateDir)}:/template`, + `${await fs.promises.realpath(resultDir)}:/result`, + ]), + }, + Volumes: { + '/template': {}, + '/result': {}, + }, + }), + ); + }); + + it('throws a correct error if the templating fails in docker', async () => { + mockDocker.run.mockResolvedValueOnce([ + { + Error: new Error('Something went wrong with docker'), + StatusCode: 0, + }, + ]); + + await expect( + runDockerContainer({ imageName, args, templateDir, resultDir }), + ).rejects.toThrow(/Something went wrong with docker/); + }); + + it('throws a correct error when the response code of the container is non-zero', async () => { + mockDocker.run.mockResolvedValueOnce([ + { + Error: null, + StatusCode: 123, + }, + ]); + + await expect( + runDockerContainer({ imageName, args, templateDir, resultDir }), + ).rejects.toThrow( + /Docker container returned a non-zero exit code \(123\)/, + ); + }); + + it('should pass through the log stream to the docker client', async () => { + const logStream = new PassThrough(); + await runDockerContainer({ + imageName, + args, + templateDir, + resultDir, + logStream, + }); + + expect(mockDocker.run).toHaveBeenCalledWith( + imageName, + args, + logStream, + expect.objectContaining({ + HostConfig: { + Binds: expect.arrayContaining([ + `${await fs.promises.realpath(templateDir)}:/template`, + `${await fs.promises.realpath(resultDir)}:/result`, + ]), + }, + Volumes: { + '/template': {}, + '/result': {}, + }, + }), + ); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/templater/helpers.ts new file mode 100644 index 0000000000..f60dcfc664 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/templater/helpers.ts @@ -0,0 +1,75 @@ +/* + * 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 Docker from 'dockerode'; +import { Writable, PassThrough } from 'stream'; +import fs from 'fs'; + +export type RunDockerContainerOptions = { + imageName: string; + args: string[]; + logStream?: Writable; + resultDir: string; + templateDir: string; +}; + +const dockerClient = new Docker(); +/** + * + * @param options the options object + * @param options.imageName the image to run + * @param options.args the arguments to pass the container + * @param options.logStream the log streamer to capture log messages + * @param options.resultDir the /result path inside the container + * @param options.templateDir the /template path inside the container + */ +export const runDockerContainer = async ({ + imageName, + args, + logStream = new PassThrough(), + resultDir, + templateDir, +}: RunDockerContainerOptions) => { + const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run( + imageName, + args, + logStream, + { + Volumes: { '/result': {}, '/template': {} }, + HostConfig: { + Binds: [ + // Need to use realpath here as Docker mounting does not like + // symlinks for binding volumes + `${await fs.promises.realpath(resultDir)}:/result`, + `${await fs.promises.realpath(templateDir)}:/template`, + ], + }, + }, + ); + + if (error) { + throw new Error( + `Docker failed to run with the following error message: ${error}`, + ); + } + + if (statusCode !== 0) { + throw new Error( + `Docker container returned a non-zero exit code (${statusCode})`, + ); + } + + return { error, statusCode }; +}; diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/templaters.ts b/plugins/scaffolder-backend/src/scaffolder/templater/templaters.ts new file mode 100644 index 0000000000..235a699a95 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/templater/templaters.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PreparerBase, RemoteProtocol, PreparerBuilder } from './types'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { parseLocationAnnotation } from './helpers'; + +export class Templaters implements PreparerBuilder { + private templaterMap = new Map(); + + register(protocol: RemoteProtocol, preparer: PreparerBase) { + this.templaterMap.set(protocol, preparer); + } + + get(template: TemplateEntityV1alpha1): PreparerBase { + const { protocol } = parseLocationAnnotation(template); + const preparer = this.templaterMap.get(protocol); + + if (!preparer) { + throw new Error(`No preparer registered for type: "${protocol}"`); + } + + return preparer; + } +} From 7ead8675dd130af527830d6e71049c8bc58bc2f9 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 24 Jun 2020 10:47:43 +0200 Subject: [PATCH 5/6] chore(scaffolder): code review comments, passing down the docker client --- packages/backend/package.json | 2 + packages/backend/src/plugins/scaffolder.ts | 4 +- plugins/scaffolder-backend/src/index.ts | 1 + .../scaffolder/templater/cookiecutter.test.ts | 43 ++++++++++++++++--- .../src/scaffolder/templater/cookiecutter.ts | 5 +++ .../src/scaffolder/templater/helpers.test.ts | 40 +++++++++-------- .../src/scaffolder/templater/helpers.ts | 6 ++- .../src/scaffolder/templater/index.ts | 8 ++-- .../scaffolder-backend/src/service/router.ts | 5 ++- 9 files changed, 82 insertions(+), 32 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index a4d5f059f9..2ae211fd8e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -26,6 +26,7 @@ "@backstage/plugin-identity-backend": "^0.1.1-alpha.10", "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.10", "@backstage/plugin-sentry-backend": "^0.1.1-alpha.10", + "dockerode": "^3.2.0", "express": "^4.17.1", "knex": "^0.21.1", "sqlite3": "^4.2.0", @@ -33,6 +34,7 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.10", + "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", "@types/helmet": "^0.0.47" diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 95d28eacb1..1b0ee46969 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -22,15 +22,17 @@ import { Preparers, } from '@backstage/plugin-scaffolder-backend'; import type { PluginEnvironment } from '../types'; +import Docker from 'dockerode'; export default async function createPlugin({ logger }: PluginEnvironment) { const templater = new CookieCutter(); const filePreparer = new FilePreparer(); const githubPreparer = new GithubPreparer(); const preparers = new Preparers(); + const dockerClient = new Docker(); preparers.register('file', filePreparer); preparers.register('github', githubPreparer); - return await createRouter({ preparers, templater, logger }); + return await createRouter({ preparers, templater, logger, dockerClient }); } diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts index c461bfede6..0a0a4cb95f 100644 --- a/plugins/scaffolder-backend/src/index.ts +++ b/plugins/scaffolder-backend/src/index.ts @@ -16,3 +16,4 @@ export * from './scaffolder'; export * from './service/router'; + diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts index bba3705a41..7c78badff1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts @@ -20,18 +20,21 @@ import fs from 'fs-extra'; import os from 'os'; import { RunDockerContainerOptions } from './helpers'; import { PassThrough } from 'stream'; +import Docker from 'dockerode'; describe('CookieCutter Templater', () => { const cookie = new CookieCutter(); - + const mockDocker = {} as Docker; const { runDockerContainer, }: { runDockerContainer: jest.Mock; } = require('./helpers'); - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); + + await fs.remove(`${os.tmpdir()}/cookiecutter.json`); }); it('should write a cookiecutter.json file with the values from the entitiy', async () => { @@ -42,7 +45,7 @@ describe('CookieCutter Templater', () => { description: 'description', }; - await cookie.run({ directory: tempdir, values }); + await cookie.run({ directory: tempdir, values, dockerClient: mockDocker }); const cookieCutterJson = await fs.readJSON(`${tempdir}/cookiecutter.json`); @@ -61,13 +64,28 @@ describe('CookieCutter Templater', () => { description: 'im something cool', }; - await cookie.run({ directory: tempdir, values }); + await cookie.run({ directory: tempdir, values, dockerClient: mockDocker }); const cookieCutterJson = await fs.readJSON(`${tempdir}/cookiecutter.json`); expect(cookieCutterJson).toEqual({ ...existingJson, ...values }); }); + it('should throw an error if the cookiecutter json is malformed and not missing', async () => { + const tempdir = os.tmpdir(); + + await fs.writeFile(`${tempdir}/cookiecutter.json`, "{'"); + + const values = { + component_id: 'hello', + description: 'im something cool', + }; + + await expect( + cookie.run({ directory: tempdir, values, dockerClient: mockDocker }), + ).rejects.toThrow(/Unexpected token ' in JSON at position 1/); + }); + it('should run the correct docker container with the correct bindings for the volumes', async () => { const tempdir = os.tmpdir(); @@ -76,7 +94,7 @@ describe('CookieCutter Templater', () => { description: 'description', }; - await cookie.run({ directory: tempdir, values }); + await cookie.run({ directory: tempdir, values, dockerClient: mockDocker }); expect(runDockerContainer).toHaveBeenCalledWith({ imageName: 'backstage/cookiecutter', @@ -84,6 +102,7 @@ describe('CookieCutter Templater', () => { templateDir: tempdir, resultDir: `${tempdir}/result`, logStream: undefined, + dockerClient: mockDocker, }); }); it('should return the result path to the end templated folder', async () => { @@ -94,7 +113,11 @@ describe('CookieCutter Templater', () => { description: 'description', }; - const path = await cookie.run({ directory: tempdir, values }); + const path = await cookie.run({ + directory: tempdir, + values, + dockerClient: mockDocker, + }); expect(path).toBe(`${tempdir}/result`); }); @@ -109,7 +132,12 @@ describe('CookieCutter Templater', () => { description: 'description', }; - await cookie.run({ directory: tempdir, values, logStream: stream }); + await cookie.run({ + directory: tempdir, + values, + logStream: stream, + dockerClient: mockDocker, + }); expect(runDockerContainer).toHaveBeenCalledWith({ imageName: 'backstage/cookiecutter', @@ -117,6 +145,7 @@ describe('CookieCutter Templater', () => { templateDir: tempdir, resultDir: `${tempdir}/result`, logStream: stream, + dockerClient: mockDocker, }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts index 70a0cc28af..a95eee7fb4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts @@ -26,6 +26,10 @@ export class CookieCutter implements TemplaterBase { try { return await fs.readJSON(`${directory}/cookiecutter.json`); } catch (ex) { + if (ex.code !== 'ENOENT') { + throw ex; + } + return {}; } } @@ -55,6 +59,7 @@ export class CookieCutter implements TemplaterBase { templateDir, resultDir, logStream: options.logStream, + dockerClient: options.dockerClient, }); return resultDir; diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/templater/helpers.test.ts index a5ac00165e..d27eb4f93f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/helpers.test.ts @@ -16,26 +16,16 @@ import Stream, { PassThrough } from 'stream'; import os from 'os'; import fs from 'fs'; - -const mockDocker = { - run: jest.fn(() => [{ Error: null, StatusCode: 0 }]), -}; - -jest.mock( - 'dockerode', - () => - class { - constructor() { - return mockDocker; - } - }, -); - +import Docker from 'dockerode'; import { runDockerContainer } from './helpers'; describe('helpers', () => { + const mockDocker = new Docker() as jest.Mocked; + beforeEach(() => { - jest.clearAllMocks(); + jest + .spyOn(mockDocker, 'run') + .mockResolvedValue([{ Error: null, StatusCode: 0 }]); }); describe('runDockerContainer', () => { @@ -50,6 +40,7 @@ describe('helpers', () => { args, templateDir, resultDir, + dockerClient: mockDocker, }); expect(mockDocker.run).toHaveBeenCalledWith( @@ -80,7 +71,13 @@ describe('helpers', () => { ]); await expect( - runDockerContainer({ imageName, args, templateDir, resultDir }), + runDockerContainer({ + imageName, + args, + templateDir, + resultDir, + dockerClient: mockDocker, + }), ).rejects.toThrow(/Something went wrong with docker/); }); @@ -93,7 +90,13 @@ describe('helpers', () => { ]); await expect( - runDockerContainer({ imageName, args, templateDir, resultDir }), + runDockerContainer({ + imageName, + args, + templateDir, + resultDir, + dockerClient: mockDocker, + }), ).rejects.toThrow( /Docker container returned a non-zero exit code \(123\)/, ); @@ -107,6 +110,7 @@ describe('helpers', () => { templateDir, resultDir, logStream, + dockerClient: mockDocker, }); expect(mockDocker.run).toHaveBeenCalledWith( diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/templater/helpers.ts index f60dcfc664..2856a016e4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/helpers.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Docker from 'dockerode'; import { Writable, PassThrough } from 'stream'; +import Docker from 'dockerode'; import fs from 'fs'; export type RunDockerContainerOptions = { @@ -23,9 +23,9 @@ export type RunDockerContainerOptions = { logStream?: Writable; resultDir: string; templateDir: string; + dockerClient: Docker; }; -const dockerClient = new Docker(); /** * * @param options the options object @@ -34,6 +34,7 @@ const dockerClient = new Docker(); * @param options.logStream the log streamer to capture log messages * @param options.resultDir the /result path inside the container * @param options.templateDir the /template path inside the container + * @param options.dockerClient the dockerClient to use */ export const runDockerContainer = async ({ imageName, @@ -41,6 +42,7 @@ export const runDockerContainer = async ({ logStream = new PassThrough(), resultDir, templateDir, + dockerClient, }: RunDockerContainerOptions) => { const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run( imageName, diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/index.ts b/plugins/scaffolder-backend/src/scaffolder/templater/index.ts index 0c255354fe..8885e65308 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/index.ts @@ -15,6 +15,7 @@ */ import type { Writable } from 'stream'; +import Docker from 'dockerode'; export interface RequiredTemplateValues { component_id: string; @@ -24,12 +25,13 @@ export interface TemplaterRunOptions { directory: string; values: RequiredTemplateValues & object; logStream?: Writable; + dockerClient: Docker; } -export abstract class TemplaterBase { +export type TemplaterBase = { // runs the templating with the values and returns the directory to push the VCS - abstract async run(opts: TemplaterRunOptions): Promise; -} + run(opts: TemplaterRunOptions): Promise; +}; export interface TemplaterConfig { templater?: TemplaterBase; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 16b119da02..ab39864e76 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -19,18 +19,20 @@ import Router from 'express-promise-router'; import express from 'express'; import { PreparerBuilder, TemplaterBase } from '../scaffolder'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import Docker from 'dockerode'; export interface RouterOptions { preparers: PreparerBuilder; templater: TemplaterBase; logger: Logger; + dockerClient: Docker; } export async function createRouter( options: RouterOptions, ): Promise { const router = Router(); - const { preparers, templater, logger: parentLogger } = options; + const { preparers, templater, logger: parentLogger, dockerClient } = options; const logger = parentLogger.child({ plugin: 'scaffolder' }); router.post('/v1/jobs', async (_, res) => { @@ -72,6 +74,7 @@ export async function createRouter( const templatedPath = await templater.run({ directory: skeletonPath, values: { component_id: 'test' }, + dockerClient, }); console.warn(templatedPath); From d581284890a653c1fbb7247cecd4effefff224e6 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 24 Jun 2020 14:01:13 +0200 Subject: [PATCH 6/6] chore(scaffolder): fixing the type deps --- plugins/scaffolder-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 061f6ca814..c8c40d7b67 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -25,6 +25,7 @@ "@backstage/catalog-model": "^0.1.1-alpha.12", "@backstage/config": "^0.1.1-alpha.12", "@types/express": "^4.17.6", + "@types/dockerode": "^2.5.32", "compression": "^1.7.4", "cors": "^2.8.5", "dockerode": "^3.2.0", @@ -40,7 +41,6 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", - "@types/dockerode": "^2.5.32", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", "@types/nodegit": "0.26.5",