diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index ab7ab1f552..4ce37999d6 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -40,6 +40,7 @@ "@octokit/rest": "^18.5.3", "@types/express": "^4.17.6", "@types/git-url-parse": "^9.0.0", + "@types/nunjucks": "^3.1.4", "azure-devops-node-api": "^10.1.1", "command-exists": "^1.2.9", "compression": "^1.7.4", @@ -58,6 +59,7 @@ "lodash": "^4.17.21", "luxon": "^1.26.0", "morgan": "^1.10.0", + "nunjucks": "^3.2.3", "octokit-plugin-create-pull-request": "^3.9.3", "uuid": "^8.2.0", "winston": "^3.2.1", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 46b8db72b4..71042129db 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -23,7 +23,11 @@ import { createCatalogWriteAction, } from './catalog'; import { createDebugLogAction } from './debug'; -import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch'; +import { + createFetchCookiecutterAction, + createFetchPlainAction, + createFetchTemplateAction, +} from './fetch'; import { createFilesystemDeleteAction, createFilesystemRenameAction, @@ -54,6 +58,10 @@ export const createBuiltinActions = (options: { integrations, templaters, }), + createFetchTemplateAction({ + integrations, + reader, + }), createPublishGithubAction({ integrations, }), 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 8e0f93f3ab..e06de4a2b5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts @@ -16,3 +16,4 @@ export { createFetchPlainAction } from './plain'; export { createFetchCookiecutterAction } from './cookiecutter'; +export { createFetchTemplateAction } from './template'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts new file mode 100644 index 0000000000..ea376e76ab --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -0,0 +1,121 @@ +/* + * 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 path, { resolve as resolvePath } from 'path'; +import { UrlReader } from '@backstage/backend-common'; +import { InputError } from '@backstage/errors'; +import { ScmIntegrations } from '@backstage/integration'; +import { fetchContents } from './helpers'; +import { createTemplateAction } from '../../createTemplateAction'; +import globby from 'globby'; +import nunjucks from 'nunjucks'; +import fs from 'fs-extra'; + +export function createFetchTemplateAction(options: { + reader: UrlReader; + integrations: ScmIntegrations; +}) { + const { reader, integrations } = options; + + return createTemplateAction<{ + url: string; + targetPath?: string; + values: any; + }>({ + id: 'fetch:template', + description: + "Downloads a skeleton and will template variables into the skeleton and places the result in the workspace, or optionally in a subdirectory specified by the 'targetPath' input option.", + 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 the templating engine', + type: 'object', + }, + }, + }, + }, + async handler(ctx) { + ctx.logger.info('Fetching template content from remote URL'); + const workDir = await ctx.createTemporaryDirectory(); + const templateDir = resolvePath(workDir, 'template'); + + // Finally move the template result into the task workspace + const targetPath = ctx.input.targetPath ?? './'; + const outputPath = path.resolve(ctx.workspacePath, targetPath); + if (!outputPath.startsWith(ctx.workspacePath)) { + throw new InputError( + `Fetch action targetPath may not specify a path outside the working directory`, + ); + } + + await fetchContents({ + reader, + integrations, + baseUrl: ctx.baseUrl, + fetchUrl: ctx.input.url, + outputPath: templateDir, + }); + + ctx.logger.info( + 'Fetched template, beginning templating process with values', + ctx.input.values, + ); + + const allFilesInTemplates = await globby(`*`, { cwd: templateDir }); + nunjucks.installJinjaCompat(); + const templater = nunjucks.configure({ + tags: { + variableStart: '${', + variableEnd: '}', + }, + autoescape: false, + }); + + templater.addFilter('jsonify', s => JSON.stringify(s)); + + for (const location of allFilesInTemplates) { + console.log(location); + + const filepath = templater.renderString(location, ctx.input.values); + console.log(filepath); + console.log('is', resolvePath(outputPath, filepath)); + await fs.writeFile( + resolvePath(outputPath, filepath), + templater.renderString( + await fs.readFile(resolvePath(templateDir, location), 'utf-8'), + ctx.input.values, + ), + ); + } + }, + }); +} diff --git a/yarn.lock b/yarn.lock index 27a6fcc250..96299709e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6242,6 +6242,11 @@ resolved "https://registry.npmjs.org/@types/npmlog/-/npmlog-4.1.2.tgz#d070fe6a6b78755d1092a3dc492d34c3d8f871c4" integrity sha512-4QQmOF5KlwfxJ5IGXFIudkeLCdMABz03RcUXu+LCb24zmln8QW6aDjuGl4d4XPVLf2j+FnjelHTP7dvceAFbhA== +"@types/nunjucks@^3.1.4": + version "3.1.4" + resolved "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.1.4.tgz#2f80e2fa5e7982b2131201d0b4474ab4d03d9de1" + integrity sha512-cR65PLlHKW/qxxj840dbNb3ICO+iAVQzaNKJ8TcKOVKFi+QcAkhw9SCY8VFAyU41SmJMs+2nrIN2JGhX+jYb7A== + "@types/oauth@*": version "0.9.1" resolved "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz#e17221e7f7936b0459ae7d006255dff61adca305" @@ -7130,6 +7135,11 @@ JSONStream@^1.0.4: jsonparse "^1.2.0" through ">=2.2.7 <3" +a-sync-waterfall@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7" + integrity sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA== + abab@^2.0.0, abab@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" @@ -7842,7 +7852,7 @@ arrify@^2.0.0, arrify@^2.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== -asap@^2.0.0, asap@~2.0.3: +asap@^2.0.0, asap@^2.0.3, asap@~2.0.3: version "2.0.6" resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= @@ -19532,6 +19542,15 @@ number-is-nan@^1.0.0: resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= +nunjucks@^3.2.3: + version "3.2.3" + resolved "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.3.tgz#1b33615247290e94e28263b5d855ece765648a31" + integrity sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ== + dependencies: + a-sync-waterfall "^1.0.0" + asap "^2.0.3" + commander "^5.1.0" + nwsapi@^2.0.7, nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"