From 118e4861be8d791119614ea8acaa7fbc51aed366 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Tue, 25 Aug 2020 11:27:56 +0100 Subject: [PATCH] Use cookie cutter installed on host --- plugins/scaffolder-backend/package.json | 1 + .../stages/templater/cookiecutter.ts | 43 ++++++++++++------- .../scaffolder/stages/templater/helpers.ts | 43 +++++++++++++++++++ yarn.lock | 5 +++ 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 7112d0f49d..275da3700b 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -27,6 +27,7 @@ "@octokit/rest": "^18.0.0", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", + "command-exists-promise": "^2.0.2", "compression": "^1.7.4", "cors": "^2.8.5", "dockerode": "^3.2.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index fff349cf82..21b6fc3dde 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -15,11 +15,13 @@ */ import fs from 'fs-extra'; import { JsonValue } from '@backstage/config'; -import { runDockerContainer } from './helpers'; +import { runDockerContainer, runCommand } from './helpers'; import { TemplaterBase, TemplaterRunOptions } from '.'; import path from 'path'; import { TemplaterRunResult } from './types'; +const commandExists = require('command-exists-promise'); + export class CookieCutter implements TemplaterBase { private async fetchTemplateCookieCutter( directory: string, @@ -51,21 +53,30 @@ export class CookieCutter implements TemplaterBase { const templateDir = options.directory; const resultDir = await fs.promises.mkdtemp(`${options.directory}-result`); - await runDockerContainer({ - imageName: 'spotify/backstage-cookiecutter', - args: [ - 'cookiecutter', - '--no-input', - '-o', - '/result', - '/template', - '--verbose', - ], - templateDir, - resultDir, - logStream: options.logStream, - dockerClient: options.dockerClient, - }); + const cookieCutterInstalled = await commandExists('cookiecutter'); + if (cookieCutterInstalled) { + await runCommand({ + command: 'cookiecutter', + args: ['--no-input', '-o', resultDir, templateDir, '--verbose'], + logStream: options.logStream, + }); + } else { + await runDockerContainer({ + imageName: 'spotify/backstage-cookiecutter', + args: [ + 'cookiecutter', + '--no-input', + '-o', + '/result', + '/template', + '--verbose', + ], + templateDir, + resultDir, + logStream: options.logStream, + dockerClient: options.dockerClient, + }); + } return { resultDir: path.resolve(resultDir, options.values.component_id as string), diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts index a3dea7ea5a..e71b63bfb0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts @@ -18,6 +18,7 @@ import Docker from 'dockerode'; import fs from 'fs'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { InputError } from '@backstage/backend-common'; +import { spawn } from 'child_process'; export type RunDockerContainerOptions = { imageName: string; @@ -29,6 +30,12 @@ export type RunDockerContainerOptions = { createOptions?: Docker.ContainerCreateOptions; }; +export type RunCommandOptions = { + command: string; + args: string[]; + logStream?: Writable; +}; + /** * Gets the templater key to use for templating from the entity * @param entity Template entity @@ -43,6 +50,42 @@ export const getTemplaterKey = (entity: TemplateEntityV1alpha1): string => { return templater; }; +/** + * + * @param options the options object + * @param options.command the command to run + * @param options.args the arguments to pass the command + * @param options.logStream the log streamer to capture log messages + */ +export const runCommand = async ({ + command, + args, + logStream = new PassThrough(), +}: RunCommandOptions) => { + await new Promise((resolve, reject) => { + const process = spawn(command, args); + + process.stdout.on('data', stream => { + logStream.write(stream); + }); + + process.stderr.on('data', stream => { + logStream.write(stream); + }); + + process.on('error', error => { + return reject(error); + }); + + process.on('close', code => { + if (code !== 0) { + return reject(`Command ${command} failed, exit code: ${code}`); + } + return resolve(); + }); + }); +}; + /** * * @param options the options object diff --git a/yarn.lock b/yarn.lock index 857b8b2cc0..6f725fe353 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8104,6 +8104,11 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== +command-exists-promise@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/command-exists-promise/-/command-exists-promise-2.0.2.tgz#7beecc4b218299f3c61fa69a4047aa0b36a64a99" + integrity sha512-T6PB6vdFrwnHXg/I0kivM3DqaCGZLjjYSOe0a5WgFKcz1sOnmOeIjnhQPXVXX3QjVbLyTJ85lJkX6lUpukTzaA== + commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1, commander@~2.20.3: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"