@backstage/plugin-scaffolder-backend 0.15.11 → 0.15.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -5,31 +5,32 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var errors = require('@backstage/errors');
6
6
  var catalogModel = require('@backstage/catalog-model');
7
7
  var fs = require('fs-extra');
8
- var path = require('path');
9
8
  var yaml = require('yaml');
10
9
  var backendCommon = require('@backstage/backend-common');
10
+ var path = require('path');
11
11
  var globby = require('globby');
12
- var nunjucks = require('nunjucks');
13
12
  var isbinaryfile = require('isbinaryfile');
13
+ var vm2 = require('vm2');
14
14
  var pluginScaffolderBackendModuleCookiecutter = require('@backstage/plugin-scaffolder-backend-module-cookiecutter');
15
15
  var child_process = require('child_process');
16
16
  var stream = require('stream');
17
17
  var azureDevopsNodeApi = require('azure-devops-node-api');
18
- var fetch = require('cross-fetch');
18
+ var fetch = require('node-fetch');
19
19
  var integration = require('@backstage/integration');
20
20
  var rest = require('@octokit/rest');
21
21
  var lodash = require('lodash');
22
22
  var octokitPluginCreatePullRequest = require('octokit-plugin-create-pull-request');
23
23
  var node = require('@gitbeaker/node');
24
24
  var webhooks = require('@octokit/webhooks');
25
- var express = require('express');
26
- var Router = require('express-promise-router');
27
- var jsonschema = require('jsonschema');
28
25
  var uuid = require('uuid');
29
26
  var luxon = require('luxon');
30
- var os = require('os');
31
27
  var Handlebars = require('handlebars');
32
28
  var winston = require('winston');
29
+ var jsonschema = require('jsonschema');
30
+ var nunjucks = require('nunjucks');
31
+ var express = require('express');
32
+ var Router = require('express-promise-router');
33
+ var os = require('os');
33
34
  var pluginCatalogBackend = require('@backstage/plugin-catalog-backend');
34
35
  var pluginScaffolderCommon = require('@backstage/plugin-scaffolder-common');
35
36
 
@@ -56,17 +57,16 @@ function _interopNamespace(e) {
56
57
  }
57
58
 
58
59
  var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
59
- var path__namespace = /*#__PURE__*/_interopNamespace(path);
60
- var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
61
60
  var yaml__namespace = /*#__PURE__*/_interopNamespace(yaml);
61
+ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
62
62
  var globby__default = /*#__PURE__*/_interopDefaultLegacy(globby);
63
- var nunjucks__default = /*#__PURE__*/_interopDefaultLegacy(nunjucks);
64
63
  var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
64
+ var Handlebars__namespace = /*#__PURE__*/_interopNamespace(Handlebars);
65
+ var winston__namespace = /*#__PURE__*/_interopNamespace(winston);
66
+ var nunjucks__default = /*#__PURE__*/_interopDefaultLegacy(nunjucks);
65
67
  var express__default = /*#__PURE__*/_interopDefaultLegacy(express);
66
68
  var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
67
69
  var os__default = /*#__PURE__*/_interopDefaultLegacy(os);
68
- var Handlebars__namespace = /*#__PURE__*/_interopNamespace(Handlebars);
69
- var winston__namespace = /*#__PURE__*/_interopNamespace(winston);
70
70
 
71
71
  const createTemplateAction = (templateAction) => {
72
72
  return templateAction;
@@ -182,7 +182,7 @@ function createCatalogWriteAction() {
182
182
  async handler(ctx) {
183
183
  ctx.logStream.write(`Writing catalog-info.yaml`);
184
184
  const {entity} = ctx.input;
185
- await fs__default['default'].writeFile(path.resolve(ctx.workspacePath, "catalog-info.yaml"), yaml__namespace.stringify(entity));
185
+ await fs__default['default'].writeFile(backendCommon.resolveSafeChildPath(ctx.workspacePath, "catalog-info.yaml"), yaml__namespace.stringify(entity));
186
186
  }
187
187
  });
188
188
  }
@@ -226,7 +226,7 @@ ${files.map((f) => ` - ${path.relative(ctx.workspacePath, f)}`).join("\n")}`);
226
226
  async function recursiveReadDir(dir) {
227
227
  const subdirs = await fs.readdir(dir);
228
228
  const files = await Promise.all(subdirs.map(async (subdir) => {
229
- const res = path.resolve(dir, subdir);
229
+ const res = path.join(dir, subdir);
230
230
  return (await fs.stat(res)).isDirectory() ? recursiveReadDir(res) : [res];
231
231
  }));
232
232
  return files.reduce((a, f) => a.concat(f), []);
@@ -250,7 +250,7 @@ async function fetchContents({
250
250
  }
251
251
  if (!fetchUrlIsAbsolute && (baseUrl == null ? void 0 : baseUrl.startsWith("file://"))) {
252
252
  const basePath = baseUrl.slice("file://".length);
253
- const srcDir = backendCommon.resolveSafeChildPath(path__namespace.dirname(basePath), fetchUrl);
253
+ const srcDir = backendCommon.resolveSafeChildPath(path__default['default'].dirname(basePath), fetchUrl);
254
254
  await fs__default['default'].copy(srcDir, outputPath);
255
255
  } else {
256
256
  let readUrl;
@@ -313,7 +313,100 @@ function createFetchPlainAction(options) {
313
313
  });
314
314
  }
315
315
 
316
- nunjucks__default['default'].installJinjaCompat();
316
+ const mkScript = (nunjucksSource) => `
317
+ const { render, renderCompat } = (() => {
318
+ const module = {};
319
+ const process = { env: {} };
320
+ const require = (pkg) => { if (pkg === 'events') { return function (){}; }};
321
+
322
+ ${nunjucksSource}
323
+
324
+ const env = module.exports.configure({
325
+ autoescape: false,
326
+ tags: {
327
+ variableStart: '\${{',
328
+ variableEnd: '}}',
329
+ },
330
+ });
331
+
332
+ const compatEnv = module.exports.configure({
333
+ autoescape: false,
334
+ tags: {
335
+ variableStart: '{{',
336
+ variableEnd: '}}',
337
+ },
338
+ });
339
+ compatEnv.addFilter('jsonify', compatEnv.getFilter('dump'));
340
+
341
+ if (typeof parseRepoUrl !== 'undefined') {
342
+ const safeHelperRef = parseRepoUrl;
343
+
344
+ env.addFilter('parseRepoUrl', repoUrl => {
345
+ return JSON.parse(safeHelperRef(repoUrl))
346
+ });
347
+ env.addFilter('projectSlug', repoUrl => {
348
+ const { owner, repo } = JSON.parse(safeHelperRef(repoUrl));
349
+ return owner + '/' + repo;
350
+ });
351
+ }
352
+
353
+ let uninstallCompat = undefined;
354
+
355
+ function render(str, values) {
356
+ try {
357
+ if (uninstallCompat) {
358
+ uninstallCompat();
359
+ uninstallCompat = undefined;
360
+ }
361
+ return env.renderString(str, JSON.parse(values));
362
+ } catch (error) {
363
+ // Make sure errors don't leak anything
364
+ throw new Error(String(error.message));
365
+ }
366
+ }
367
+
368
+ function renderCompat(str, values) {
369
+ try {
370
+ if (!uninstallCompat) {
371
+ uninstallCompat = module.exports.installJinjaCompat();
372
+ }
373
+ return compatEnv.renderString(str, JSON.parse(values));
374
+ } catch (error) {
375
+ // Make sure errors don't leak anything
376
+ throw new Error(String(error.message));
377
+ }
378
+ }
379
+
380
+ return { render, renderCompat };
381
+ })();
382
+ `;
383
+ class SecureTemplater {
384
+ static async loadRenderer(options = {}) {
385
+ const {parseRepoUrl, cookiecutterCompat} = options;
386
+ let sandbox = void 0;
387
+ if (parseRepoUrl) {
388
+ sandbox = {
389
+ parseRepoUrl: (url) => JSON.stringify(parseRepoUrl(url))
390
+ };
391
+ }
392
+ const vm = new vm2.VM({sandbox});
393
+ const nunjucksSource = await fs__default['default'].readFile(backendCommon.resolvePackagePath("@backstage/plugin-scaffolder-backend", "assets/nunjucks.js.txt"), "utf-8");
394
+ vm.run(mkScript(nunjucksSource));
395
+ const render = (template, values) => {
396
+ if (!vm) {
397
+ throw new Error("SecureTemplater has not been initialized");
398
+ }
399
+ vm.setGlobal("templateStr", template);
400
+ vm.setGlobal("templateValues", JSON.stringify(values));
401
+ if (cookiecutterCompat) {
402
+ return vm.run(`renderCompat(templateStr, templateValues)`);
403
+ }
404
+ return vm.run(`render(templateStr, templateValues)`);
405
+ };
406
+ return render;
407
+ }
408
+ }
409
+
317
410
  function createFetchTemplateAction(options) {
318
411
  const {reader, integrations} = options;
319
412
  return createTemplateAction({
@@ -364,7 +457,7 @@ function createFetchTemplateAction(options) {
364
457
  var _a;
365
458
  ctx.logger.info("Fetching template content from remote URL");
366
459
  const workDir = await ctx.createTemporaryDirectory();
367
- const templateDir = path.resolve(workDir, "template");
460
+ const templateDir = backendCommon.resolveSafeChildPath(workDir, "template");
368
461
  const targetPath = (_a = ctx.input.targetPath) != null ? _a : "./";
369
462
  const outputDir = backendCommon.resolveSafeChildPath(ctx.workspacePath, targetPath);
370
463
  if (ctx.input.copyWithoutRender && !Array.isArray(ctx.input.copyWithoutRender)) {
@@ -400,23 +493,14 @@ function createFetchTemplateAction(options) {
400
493
  onlyFiles: false,
401
494
  markDirectories: true
402
495
  })))).flat());
403
- const templater = nunjucks__default['default'].configure({
404
- ...ctx.input.cookiecutterCompat ? {} : {
405
- tags: {
406
- variableStart: "${{",
407
- variableEnd: "}}"
408
- }
409
- },
410
- autoescape: false
411
- });
412
- if (ctx.input.cookiecutterCompat) {
413
- templater.addFilter("jsonify", templater.getFilter("dump"));
414
- }
415
496
  const {cookiecutterCompat, values} = ctx.input;
416
497
  const context = {
417
498
  [cookiecutterCompat ? "cookiecutter" : "values"]: values
418
499
  };
419
500
  ctx.logger.info(`Processing ${allEntriesInTemplate.length} template files/directories with input values`, ctx.input.values);
501
+ const renderTemplate = await SecureTemplater.loadRenderer({
502
+ cookiecutterCompat: ctx.input.cookiecutterCompat
503
+ });
420
504
  for (const location of allEntriesInTemplate) {
421
505
  let renderFilename;
422
506
  let renderContents;
@@ -431,9 +515,12 @@ function createFetchTemplateAction(options) {
431
515
  renderFilename = renderContents = !nonTemplatedEntries.has(location);
432
516
  }
433
517
  if (renderFilename) {
434
- localOutputPath = templater.renderString(localOutputPath, context);
518
+ localOutputPath = renderTemplate(localOutputPath, context);
519
+ }
520
+ const outputPath = backendCommon.resolveSafeChildPath(outputDir, localOutputPath);
521
+ if (outputDir === outputPath) {
522
+ continue;
435
523
  }
436
- const outputPath = path.resolve(outputDir, localOutputPath);
437
524
  if (!renderContents && !extension) {
438
525
  ctx.logger.info(`Copying file/directory ${location} without processing.`);
439
526
  }
@@ -441,7 +528,7 @@ function createFetchTemplateAction(options) {
441
528
  ctx.logger.info(`Writing directory ${location} to template output path.`);
442
529
  await fs__default['default'].ensureDir(outputPath);
443
530
  } else {
444
- const inputFilePath = path.resolve(templateDir, location);
531
+ const inputFilePath = backendCommon.resolveSafeChildPath(templateDir, location);
445
532
  if (await isbinaryfile.isBinaryFile(inputFilePath)) {
446
533
  ctx.logger.info(`Copying binary file ${location} to template output path.`);
447
534
  await fs__default['default'].copy(inputFilePath, outputPath);
@@ -449,7 +536,7 @@ function createFetchTemplateAction(options) {
449
536
  const statsObj = await fs__default['default'].stat(inputFilePath);
450
537
  ctx.logger.info(`Writing file ${location} to template output path with mode ${statsObj.mode}.`);
451
538
  const inputFileContents = await fs__default['default'].readFile(inputFilePath, "utf-8");
452
- await fs__default['default'].outputFile(outputPath, renderContents ? templater.renderString(inputFileContents, context) : inputFileContents, {mode: statsObj.mode});
539
+ await fs__default['default'].outputFile(outputPath, renderContents ? renderTemplate(inputFileContents, context) : inputFileContents, {mode: statsObj.mode});
453
540
  }
454
541
  }
455
542
  }
@@ -560,10 +647,11 @@ const createFilesystemRenameAction = () => {
560
647
  const runCommand = async ({
561
648
  command,
562
649
  args,
563
- logStream = new stream.PassThrough()
650
+ logStream = new stream.PassThrough(),
651
+ options
564
652
  }) => {
565
653
  await new Promise((resolve, reject) => {
566
- const process = child_process.spawn(command, args);
654
+ const process = child_process.spawn(command, args, options);
567
655
  process.stdout.on("data", (stream) => {
568
656
  logStream.write(stream);
569
657
  });
@@ -714,6 +802,11 @@ const parseRepoUrl = (repoUrl, integrations) => {
714
802
  }
715
803
  return {host, owner, repo, organization, workspace, project};
716
804
  };
805
+ const isExecutable = (fileMode) => {
806
+ const executeBitMask = 73;
807
+ const res = fileMode & executeBitMask;
808
+ return res > 0;
809
+ };
717
810
 
718
811
  function createPublishAzureAction(options) {
719
812
  const {integrations, config} = options;
@@ -1392,11 +1485,10 @@ const createPublishGithubPullRequestAction = ({
1392
1485
  dot: true
1393
1486
  });
1394
1487
  const fileContents = await Promise.all(localFilePaths.map((filePath) => {
1395
- const absPath = path__default['default'].resolve(fileRoot, filePath);
1488
+ const absPath = backendCommon.resolveSafeChildPath(fileRoot, filePath);
1396
1489
  const base64EncodedContent = fs__default['default'].readFileSync(absPath).toString("base64");
1397
1490
  const fileStat = fs__default['default'].statSync(absPath);
1398
- const isExecutable = fileStat.mode === 33277;
1399
- const githubTreeItemMode = isExecutable ? "100755" : "100644";
1491
+ const githubTreeItemMode = isExecutable(fileStat.mode) ? "100755" : "100644";
1400
1492
  const encoding = "base64";
1401
1493
  return {
1402
1494
  encoding,
@@ -1747,38 +1839,16 @@ class TemplateActionRegistry {
1747
1839
  }
1748
1840
  }
1749
1841
 
1750
- class CatalogEntityClient {
1751
- constructor(catalogClient) {
1752
- this.catalogClient = catalogClient;
1753
- }
1754
- async findTemplate(templateName, options) {
1755
- const {items: templates} = await this.catalogClient.getEntities({
1756
- filter: {
1757
- kind: "template",
1758
- "metadata.name": templateName
1759
- }
1760
- }, options);
1761
- if (templates.length !== 1) {
1762
- if (templates.length > 1) {
1763
- throw new errors.ConflictError("Templates lookup resulted in multiple matches");
1764
- } else {
1765
- throw new errors.NotFoundError("Template not found");
1766
- }
1767
- }
1768
- return templates[0];
1769
- }
1770
- }
1771
-
1772
1842
  const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-scaffolder-backend", "migrations");
1773
1843
  class DatabaseTaskStore {
1774
- constructor(db) {
1775
- this.db = db;
1776
- }
1777
- static async create(knex) {
1778
- await knex.migrate.latest({
1844
+ static async create(options) {
1845
+ await options.database.migrate.latest({
1779
1846
  directory: migrationsDir
1780
1847
  });
1781
- return new DatabaseTaskStore(knex);
1848
+ return new DatabaseTaskStore(options);
1849
+ }
1850
+ constructor(options) {
1851
+ this.db = options.database;
1782
1852
  }
1783
1853
  async getTask(taskId) {
1784
1854
  const [result] = await this.db("tasks").where({id: taskId}).select();
@@ -1934,7 +2004,7 @@ class DatabaseTaskStore {
1934
2004
  }
1935
2005
  }
1936
2006
 
1937
- class TaskAgent {
2007
+ class TaskManager {
1938
2008
  constructor(state, storage, logger) {
1939
2009
  this.state = state;
1940
2010
  this.storage = storage;
@@ -1942,7 +2012,7 @@ class TaskAgent {
1942
2012
  this.isDone = false;
1943
2013
  }
1944
2014
  static create(state, storage, logger) {
1945
- const agent = new TaskAgent(state, storage, logger);
2015
+ const agent = new TaskManager(state, storage, logger);
1946
2016
  agent.startTimeout();
1947
2017
  return agent;
1948
2018
  }
@@ -2008,7 +2078,7 @@ class StorageTaskBroker {
2008
2078
  for (; ; ) {
2009
2079
  const pendingTask = await this.storage.claimTask();
2010
2080
  if (pendingTask) {
2011
- return TaskAgent.create({
2081
+ return TaskManager.create({
2012
2082
  taskId: pendingTask.id,
2013
2083
  spec: pendingTask.spec,
2014
2084
  secrets: pendingTask.secrets
@@ -2050,7 +2120,7 @@ class StorageTaskBroker {
2050
2120
  await new Promise((resolve) => setTimeout(resolve, 1e3));
2051
2121
  }
2052
2122
  })();
2053
- return unsubscribe;
2123
+ return {unsubscribe};
2054
2124
  }
2055
2125
  async vacuumTasks(timeoutS) {
2056
2126
  const {tasks} = await this.storage.listStaleTasks(timeoutS);
@@ -2077,70 +2147,12 @@ class StorageTaskBroker {
2077
2147
  }
2078
2148
  }
2079
2149
 
2080
- class TaskWorker {
2081
- constructor(options) {
2082
- this.options = options;
2083
- }
2084
- start() {
2085
- (async () => {
2086
- for (; ; ) {
2087
- const task = await this.options.taskBroker.claim();
2088
- await this.runOneTask(task);
2089
- }
2090
- })();
2091
- }
2092
- async runOneTask(task) {
2093
- try {
2094
- const {output} = task.spec.apiVersion === "scaffolder.backstage.io/v1beta3" ? await this.options.runners.workflowRunner.execute(task) : await this.options.runners.legacyWorkflowRunner.execute(task);
2095
- await task.complete("completed", {output});
2096
- } catch (error) {
2097
- errors.assertError(error);
2098
- await task.complete("failed", {
2099
- error: {name: error.name, message: error.message}
2100
- });
2101
- }
2102
- }
2103
- }
2104
-
2105
- async function getWorkingDirectory(config, logger) {
2106
- if (!config.has("backend.workingDirectory")) {
2107
- return os__default['default'].tmpdir();
2108
- }
2109
- const workingDirectory = config.getString("backend.workingDirectory");
2110
- try {
2111
- await fs__default['default'].access(workingDirectory, fs__default['default'].constants.F_OK | fs__default['default'].constants.W_OK);
2112
- logger.info(`using working directory: ${workingDirectory}`);
2113
- } catch (err) {
2114
- errors.assertError(err);
2115
- logger.error(`working directory ${workingDirectory} ${err.code === "ENOENT" ? "does not exist" : "is not writable"}`);
2116
- throw err;
2117
- }
2118
- return workingDirectory;
2119
- }
2120
- function getEntityBaseUrl(entity) {
2121
- var _a, _b;
2122
- let location = (_a = entity.metadata.annotations) == null ? void 0 : _a[catalogModel.SOURCE_LOCATION_ANNOTATION];
2123
- if (!location) {
2124
- location = (_b = entity.metadata.annotations) == null ? void 0 : _b[catalogModel.LOCATION_ANNOTATION];
2125
- }
2126
- if (!location) {
2127
- return void 0;
2128
- }
2129
- const {type, target} = catalogModel.parseLocationReference(location);
2130
- if (type === "url") {
2131
- return target;
2132
- } else if (type === "file") {
2133
- return `file://${target}`;
2134
- }
2135
- return void 0;
2136
- }
2137
-
2138
2150
  function isTruthy(value) {
2139
2151
  return lodash.isArray(value) ? value.length > 0 : !!value;
2140
2152
  }
2141
2153
 
2142
2154
  const isValidTaskSpec$1 = (taskSpec) => taskSpec.apiVersion === "backstage.io/v1beta2";
2143
- class LegacyWorkflowRunner {
2155
+ class HandlebarsWorkflowRunner {
2144
2156
  constructor(options) {
2145
2157
  this.options = options;
2146
2158
  this.handlebars = Handlebars__namespace.create();
@@ -2251,6 +2263,9 @@ class LegacyWorkflowRunner {
2251
2263
  this.options.logger.debug(`Running ${action.id} with input`, {
2252
2264
  input: JSON.stringify(input, null, 2)
2253
2265
  });
2266
+ if (!task.spec.metadata) {
2267
+ console.warn("DEPRECATION NOTICE: metadata is undefined. metadata will be required in the future.");
2268
+ }
2254
2269
  await action.handler({
2255
2270
  baseUrl: task.spec.baseUrl,
2256
2271
  logger: taskLogger,
@@ -2265,7 +2280,8 @@ class LegacyWorkflowRunner {
2265
2280
  },
2266
2281
  output(name, value) {
2267
2282
  stepOutputs[name] = value;
2268
- }
2283
+ },
2284
+ metadata: task.spec.metadata
2269
2285
  });
2270
2286
  for (const tmpDir of tmpDirs) {
2271
2287
  await fs__default['default'].remove(tmpDir);
@@ -2316,7 +2332,10 @@ class LegacyWorkflowRunner {
2316
2332
  const isValidTaskSpec = (taskSpec) => {
2317
2333
  return taskSpec.apiVersion === "scaffolder.backstage.io/v1beta3";
2318
2334
  };
2319
- const createStepLogger = ({task, step}) => {
2335
+ const createStepLogger = ({
2336
+ task,
2337
+ step
2338
+ }) => {
2320
2339
  const metadata = {stepId: step.id};
2321
2340
  const taskLogger = winston__namespace.createLogger({
2322
2341
  level: process.env.LOG_LEVEL || "info",
@@ -2333,38 +2352,30 @@ const createStepLogger = ({task, step}) => {
2333
2352
  taskLogger.add(new winston__namespace.transports.Stream({stream: streamLogger}));
2334
2353
  return {taskLogger, streamLogger};
2335
2354
  };
2336
- class DefaultWorkflowRunner {
2355
+ class NunjucksWorkflowRunner {
2337
2356
  constructor(options) {
2338
2357
  this.options = options;
2339
- this.nunjucksOptions = {
2358
+ }
2359
+ isSingleTemplateString(input) {
2360
+ var _a, _b;
2361
+ const {parser, nodes} = nunjucks__default['default'];
2362
+ const parsed = parser.parse(input, {}, {
2340
2363
  autoescape: false,
2341
2364
  tags: {
2342
2365
  variableStart: "${{",
2343
2366
  variableEnd: "}}"
2344
2367
  }
2345
- };
2346
- this.nunjucks = nunjucks__default['default'].configure(this.nunjucksOptions);
2347
- this.nunjucks.addFilter("parseRepoUrl", (repoUrl) => {
2348
- return parseRepoUrl(repoUrl, this.options.integrations);
2349
- });
2350
- this.nunjucks.addFilter("projectSlug", (repoUrl) => {
2351
- const {owner, repo} = parseRepoUrl(repoUrl, this.options.integrations);
2352
- return `${owner}/${repo}`;
2353
2368
  });
2369
+ return parsed.children.length === 1 && !(((_b = (_a = parsed.children[0]) == null ? void 0 : _a.children) == null ? void 0 : _b[0]) instanceof nodes.TemplateData);
2354
2370
  }
2355
- isSingleTemplateString(input) {
2356
- const {parser, nodes} = require("nunjucks");
2357
- const parsed = parser.parse(input, {}, this.nunjucksOptions);
2358
- return parsed.children.length === 1 && !(parsed.children[0] instanceof nodes.TemplateData);
2359
- }
2360
- render(input, context) {
2371
+ render(input, context, renderTemplate) {
2361
2372
  return JSON.parse(JSON.stringify(input), (_key, value) => {
2362
2373
  try {
2363
2374
  if (typeof value === "string") {
2364
2375
  try {
2365
2376
  if (this.isSingleTemplateString(value)) {
2366
2377
  const wrappedDumped = value.replace(/\${{(.+)}}/g, "${{ ( $1 ) | dump }}");
2367
- const templated2 = this.nunjucks.renderString(wrappedDumped, context);
2378
+ const templated2 = renderTemplate(wrappedDumped, context);
2368
2379
  if (templated2 === "") {
2369
2380
  return void 0;
2370
2381
  }
@@ -2373,7 +2384,7 @@ class DefaultWorkflowRunner {
2373
2384
  } catch (ex) {
2374
2385
  this.options.logger.error(`Failed to parse template string: ${value} with error ${ex.message}`);
2375
2386
  }
2376
- const templated = this.nunjucks.renderString(value, context);
2387
+ const templated = renderTemplate(value, context);
2377
2388
  if (templated === "") {
2378
2389
  return void 0;
2379
2390
  }
@@ -2391,6 +2402,12 @@ class DefaultWorkflowRunner {
2391
2402
  throw new errors.InputError("Wrong template version executed with the workflow engine");
2392
2403
  }
2393
2404
  const workspacePath = path__default['default'].join(this.options.workingDirectory, await task.getWorkspaceName());
2405
+ const {integrations} = this.options;
2406
+ const renderTemplate = await SecureTemplater.loadRenderer({
2407
+ parseRepoUrl(url) {
2408
+ return parseRepoUrl(url, integrations);
2409
+ }
2410
+ });
2394
2411
  try {
2395
2412
  await fs__default['default'].ensureDir(workspacePath);
2396
2413
  await task.emitLog(`Starting up task with ${task.spec.steps.length} steps`);
@@ -2401,7 +2418,7 @@ class DefaultWorkflowRunner {
2401
2418
  for (const step of task.spec.steps) {
2402
2419
  try {
2403
2420
  if (step.if) {
2404
- const ifResult = await this.render(step.if, context);
2421
+ const ifResult = await this.render(step.if, context, renderTemplate);
2405
2422
  if (!isTruthy(ifResult)) {
2406
2423
  await task.emitLog(`Skipping step ${step.id} because it's if condition was false`, {stepId: step.id, status: "skipped"});
2407
2424
  continue;
@@ -2413,7 +2430,7 @@ class DefaultWorkflowRunner {
2413
2430
  });
2414
2431
  const action = this.options.actionRegistry.get(step.action);
2415
2432
  const {taskLogger, streamLogger} = createStepLogger({task, step});
2416
- const input = (_a = step.input && this.render(step.input, context)) != null ? _a : {};
2433
+ const input = (_a = step.input && this.render(step.input, context, renderTemplate)) != null ? _a : {};
2417
2434
  if ((_b = action.schema) == null ? void 0 : _b.input) {
2418
2435
  const validateResult = jsonschema.validate(input, action.schema.input);
2419
2436
  if (!validateResult.valid) {
@@ -2423,6 +2440,9 @@ class DefaultWorkflowRunner {
2423
2440
  }
2424
2441
  const tmpDirs = new Array();
2425
2442
  const stepOutput = {};
2443
+ if (!task.spec.metadata) {
2444
+ console.warn("DEPRECATION NOTICE: metadata is undefined. metadata will be required in the future.");
2445
+ }
2426
2446
  await action.handler({
2427
2447
  baseUrl: task.spec.baseUrl,
2428
2448
  input,
@@ -2436,7 +2456,8 @@ class DefaultWorkflowRunner {
2436
2456
  },
2437
2457
  output(name, value) {
2438
2458
  stepOutput[name] = value;
2439
- }
2459
+ },
2460
+ metadata: task.spec.metadata
2440
2461
  });
2441
2462
  for (const tmpDir of tmpDirs) {
2442
2463
  await fs__default['default'].remove(tmpDir);
@@ -2454,7 +2475,7 @@ class DefaultWorkflowRunner {
2454
2475
  throw err;
2455
2476
  }
2456
2477
  }
2457
- const output = this.render(task.spec.output, context);
2478
+ const output = this.render(task.spec.output, context, renderTemplate);
2458
2479
  return {output};
2459
2480
  } finally {
2460
2481
  if (workspacePath) {
@@ -2464,6 +2485,111 @@ class DefaultWorkflowRunner {
2464
2485
  }
2465
2486
  }
2466
2487
 
2488
+ class TaskWorker {
2489
+ constructor(options) {
2490
+ this.options = options;
2491
+ }
2492
+ static async create(options) {
2493
+ const {
2494
+ taskBroker,
2495
+ logger,
2496
+ actionRegistry,
2497
+ integrations,
2498
+ workingDirectory
2499
+ } = options;
2500
+ const legacyWorkflowRunner = new HandlebarsWorkflowRunner({
2501
+ logger,
2502
+ actionRegistry,
2503
+ integrations,
2504
+ workingDirectory
2505
+ });
2506
+ const workflowRunner = new NunjucksWorkflowRunner({
2507
+ actionRegistry,
2508
+ integrations,
2509
+ logger,
2510
+ workingDirectory
2511
+ });
2512
+ return new TaskWorker({
2513
+ taskBroker,
2514
+ runners: {legacyWorkflowRunner, workflowRunner}
2515
+ });
2516
+ }
2517
+ start() {
2518
+ (async () => {
2519
+ for (; ; ) {
2520
+ const task = await this.options.taskBroker.claim();
2521
+ await this.runOneTask(task);
2522
+ }
2523
+ })();
2524
+ }
2525
+ async runOneTask(task) {
2526
+ try {
2527
+ const {output} = task.spec.apiVersion === "scaffolder.backstage.io/v1beta3" ? await this.options.runners.workflowRunner.execute(task) : await this.options.runners.legacyWorkflowRunner.execute(task);
2528
+ await task.complete("completed", {output});
2529
+ } catch (error) {
2530
+ errors.assertError(error);
2531
+ await task.complete("failed", {
2532
+ error: {name: error.name, message: error.message}
2533
+ });
2534
+ }
2535
+ }
2536
+ }
2537
+
2538
+ class CatalogEntityClient {
2539
+ constructor(catalogClient) {
2540
+ this.catalogClient = catalogClient;
2541
+ }
2542
+ async findTemplate(templateName, options) {
2543
+ const {items: templates} = await this.catalogClient.getEntities({
2544
+ filter: {
2545
+ kind: "template",
2546
+ "metadata.name": templateName
2547
+ }
2548
+ }, options);
2549
+ if (templates.length !== 1) {
2550
+ if (templates.length > 1) {
2551
+ throw new errors.ConflictError("Templates lookup resulted in multiple matches");
2552
+ } else {
2553
+ throw new errors.NotFoundError("Template not found");
2554
+ }
2555
+ }
2556
+ return templates[0];
2557
+ }
2558
+ }
2559
+
2560
+ async function getWorkingDirectory(config, logger) {
2561
+ if (!config.has("backend.workingDirectory")) {
2562
+ return os__default['default'].tmpdir();
2563
+ }
2564
+ const workingDirectory = config.getString("backend.workingDirectory");
2565
+ try {
2566
+ await fs__default['default'].access(workingDirectory, fs__default['default'].constants.F_OK | fs__default['default'].constants.W_OK);
2567
+ logger.info(`using working directory: ${workingDirectory}`);
2568
+ } catch (err) {
2569
+ errors.assertError(err);
2570
+ logger.error(`working directory ${workingDirectory} ${err.code === "ENOENT" ? "does not exist" : "is not writable"}`);
2571
+ throw err;
2572
+ }
2573
+ return workingDirectory;
2574
+ }
2575
+ function getEntityBaseUrl(entity) {
2576
+ var _a, _b;
2577
+ let location = (_a = entity.metadata.annotations) == null ? void 0 : _a[catalogModel.SOURCE_LOCATION_ANNOTATION];
2578
+ if (!location) {
2579
+ location = (_b = entity.metadata.annotations) == null ? void 0 : _b[catalogModel.LOCATION_ANNOTATION];
2580
+ }
2581
+ if (!location) {
2582
+ return void 0;
2583
+ }
2584
+ const {type, target} = catalogModel.parseLocationReference(location);
2585
+ if (type === "url") {
2586
+ return target;
2587
+ } else if (type === "file") {
2588
+ return `file://${target}`;
2589
+ }
2590
+ return void 0;
2591
+ }
2592
+
2467
2593
  function isSupportedTemplate(entity) {
2468
2594
  return entity.apiVersion === "backstage.io/v1beta2" || entity.apiVersion === "scaffolder.backstage.io/v1beta3";
2469
2595
  }
@@ -2484,29 +2610,24 @@ async function createRouter(options) {
2484
2610
  const workingDirectory = await getWorkingDirectory(config, logger);
2485
2611
  const entityClient = new CatalogEntityClient(catalogClient);
2486
2612
  const integrations = integration.ScmIntegrations.fromConfig(config);
2487
- const databaseTaskStore = await DatabaseTaskStore.create(await database.getClient());
2488
- const taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
2613
+ let taskBroker;
2614
+ if (!options.taskBroker) {
2615
+ const databaseTaskStore = await DatabaseTaskStore.create({
2616
+ database: await database.getClient()
2617
+ });
2618
+ taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
2619
+ } else {
2620
+ taskBroker = options.taskBroker;
2621
+ }
2489
2622
  const actionRegistry = new TemplateActionRegistry();
2490
- const legacyWorkflowRunner = new LegacyWorkflowRunner({
2491
- logger,
2492
- actionRegistry,
2493
- integrations,
2494
- workingDirectory
2495
- });
2496
- const workflowRunner = new DefaultWorkflowRunner({
2497
- actionRegistry,
2498
- integrations,
2499
- logger,
2500
- workingDirectory
2501
- });
2502
2623
  const workers = [];
2503
2624
  for (let i = 0; i < (taskWorkers || 1); i++) {
2504
- const worker = new TaskWorker({
2625
+ const worker = await TaskWorker.create({
2505
2626
  taskBroker,
2506
- runners: {
2507
- legacyWorkflowRunner,
2508
- workflowRunner
2509
- }
2627
+ actionRegistry,
2628
+ integrations,
2629
+ logger,
2630
+ workingDirectory
2510
2631
  });
2511
2632
  workers.push(worker);
2512
2633
  }
@@ -2556,7 +2677,7 @@ async function createRouter(options) {
2556
2677
  });
2557
2678
  res.json(actionsList);
2558
2679
  }).post("/v2/tasks", async (req, res) => {
2559
- var _a, _b, _c;
2680
+ var _a, _b, _c, _d, _e;
2560
2681
  const templateName = req.body.templateName;
2561
2682
  const values = req.body.values;
2562
2683
  const token = getBearerToken(req.headers.authorization);
@@ -2585,7 +2706,8 @@ async function createRouter(options) {
2585
2706
  name: (_b2 = step.name) != null ? _b2 : step.action
2586
2707
  };
2587
2708
  }),
2588
- output: (_b = template.spec.output) != null ? _b : {}
2709
+ output: (_b = template.spec.output) != null ? _b : {},
2710
+ metadata: {name: (_c = template.metadata) == null ? void 0 : _c.name}
2589
2711
  } : {
2590
2712
  apiVersion: template.apiVersion,
2591
2713
  baseUrl,
@@ -2598,7 +2720,8 @@ async function createRouter(options) {
2598
2720
  name: (_b2 = step.name) != null ? _b2 : step.action
2599
2721
  };
2600
2722
  }),
2601
- output: (_c = template.spec.output) != null ? _c : {}
2723
+ output: (_d = template.spec.output) != null ? _d : {},
2724
+ metadata: {name: (_e = template.metadata) == null ? void 0 : _e.name}
2602
2725
  };
2603
2726
  } else {
2604
2727
  throw new errors.InputError(`Unsupported apiVersion field in schema entity, ${template.apiVersion}`);
@@ -2617,14 +2740,15 @@ async function createRouter(options) {
2617
2740
  res.status(200).json(task);
2618
2741
  }).get("/v2/tasks/:taskId/eventstream", async (req, res) => {
2619
2742
  const {taskId} = req.params;
2620
- const after = Number(req.query.after) || void 0;
2743
+ const after = req.query.after !== void 0 ? Number(req.query.after) : void 0;
2621
2744
  logger.debug(`Event stream observing taskId '${taskId}' opened`);
2622
2745
  res.writeHead(200, {
2623
2746
  Connection: "keep-alive",
2624
2747
  "Cache-Control": "no-cache",
2625
2748
  "Content-Type": "text/event-stream"
2626
2749
  });
2627
- const unsubscribe = taskBroker.observe({taskId, after}, (error, {events}) => {
2750
+ const {unsubscribe} = taskBroker.observe({taskId, after}, (error, {events}) => {
2751
+ var _a;
2628
2752
  if (error) {
2629
2753
  logger.error(`Received error from event stream when observing taskId '${taskId}', ${error}`);
2630
2754
  }
@@ -2638,7 +2762,7 @@ data: ${JSON.stringify(event)}
2638
2762
  shouldUnsubscribe = true;
2639
2763
  }
2640
2764
  }
2641
- res.flush();
2765
+ (_a = res.flush) == null ? void 0 : _a.call(res);
2642
2766
  if (shouldUnsubscribe)
2643
2767
  unsubscribe();
2644
2768
  });
@@ -2646,6 +2770,27 @@ data: ${JSON.stringify(event)}
2646
2770
  unsubscribe();
2647
2771
  logger.debug(`Event stream observing taskId '${taskId}' closed`);
2648
2772
  });
2773
+ }).get("/v2/tasks/:taskId/events", async (req, res) => {
2774
+ const {taskId} = req.params;
2775
+ const after = Number(req.query.after) || void 0;
2776
+ let unsubscribe = () => {
2777
+ };
2778
+ const timeout = setTimeout(() => {
2779
+ unsubscribe();
2780
+ res.json([]);
2781
+ }, 3e4);
2782
+ ({unsubscribe} = taskBroker.observe({taskId, after}, (error, {events}) => {
2783
+ clearTimeout(timeout);
2784
+ unsubscribe();
2785
+ if (error) {
2786
+ logger.error(`Received error from log when observing taskId '${taskId}', ${error}`);
2787
+ }
2788
+ res.json(events);
2789
+ }));
2790
+ req.on("close", () => {
2791
+ unsubscribe();
2792
+ clearTimeout(timeout);
2793
+ });
2649
2794
  });
2650
2795
  const app = express__default['default']();
2651
2796
  app.set("logger", logger);
@@ -2712,8 +2857,11 @@ Object.defineProperty(exports, 'createFetchCookiecutterAction', {
2712
2857
  }
2713
2858
  });
2714
2859
  exports.CatalogEntityClient = CatalogEntityClient;
2860
+ exports.DatabaseTaskStore = DatabaseTaskStore;
2715
2861
  exports.OctokitProvider = OctokitProvider;
2716
2862
  exports.ScaffolderEntitiesProcessor = ScaffolderEntitiesProcessor;
2863
+ exports.TaskManager = TaskManager;
2864
+ exports.TaskWorker = TaskWorker;
2717
2865
  exports.TemplateActionRegistry = TemplateActionRegistry;
2718
2866
  exports.createBuiltinActions = createBuiltinActions;
2719
2867
  exports.createCatalogRegisterAction = createCatalogRegisterAction;