@backstage/plugin-scaffolder-backend 1.20.0 → 1.21.0-next.1

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.
@@ -27,11 +27,11 @@ var gerrit = require('@backstage/plugin-scaffolder-backend-module-gerrit');
27
27
  var gitlab = require('@backstage/plugin-scaffolder-backend-module-gitlab');
28
28
  var uuid = require('uuid');
29
29
  var ObservableImpl = require('zen-observable');
30
+ var lodash = require('lodash');
30
31
  var PQueue = require('p-queue');
31
32
  var winston = require('winston');
32
33
  var nunjucks = require('nunjucks');
33
34
  var stream = require('stream');
34
- var lodash = require('lodash');
35
35
  var pluginPermissionNode = require('@backstage/plugin-permission-node');
36
36
  var promClient = require('prom-client');
37
37
  var pluginPermissionCommon = require('@backstage/plugin-permission-common');
@@ -640,6 +640,11 @@ function createFetchPlainAction(options) {
640
640
  title: "Target Path",
641
641
  description: "Target path within the working directory to download the contents to.",
642
642
  type: "string"
643
+ },
644
+ token: {
645
+ title: "Token",
646
+ description: "An optional token to use for authentication when reading the resources.",
647
+ type: "string"
643
648
  }
644
649
  }
645
650
  }
@@ -655,7 +660,8 @@ function createFetchPlainAction(options) {
655
660
  integrations,
656
661
  baseUrl: (_b = ctx.templateInfo) == null ? void 0 : _b.baseUrl,
657
662
  fetchUrl: ctx.input.url,
658
- outputPath
663
+ outputPath,
664
+ token: ctx.input.token
659
665
  });
660
666
  }
661
667
  });
@@ -700,6 +706,11 @@ function createFetchPlainFileAction(options) {
700
706
  title: "Target Path",
701
707
  description: "Target path within the working directory to download the file as.",
702
708
  type: "string"
709
+ },
710
+ token: {
711
+ title: "Token",
712
+ description: "An optional token to use for authentication when reading the resources.",
713
+ type: "string"
703
714
  }
704
715
  }
705
716
  }
@@ -717,12 +728,22 @@ function createFetchPlainFileAction(options) {
717
728
  integrations,
718
729
  baseUrl: (_a = ctx.templateInfo) == null ? void 0 : _a.baseUrl,
719
730
  fetchUrl: ctx.input.url,
720
- outputPath
731
+ outputPath,
732
+ token: ctx.input.token
721
733
  });
722
734
  }
723
735
  });
724
736
  }
725
737
 
738
+ function isNoNodeSnapshotOptionProvided() {
739
+ var _a;
740
+ return ((_a = process.env.NODE_OPTIONS) == null ? void 0 : _a.includes("--no-node-snapshot")) || process.argv.includes("--no-node-snapshot");
741
+ }
742
+ function getMajorNodeVersion() {
743
+ const version = process.versions.node;
744
+ return parseInt(version.split(".")[0], 10);
745
+ }
746
+
726
747
  const mkScript = (nunjucksSource) => `
727
748
  const { render, renderCompat } = (() => {
728
749
  const module = {};
@@ -733,6 +754,7 @@ const { render, renderCompat } = (() => {
733
754
 
734
755
  const env = module.exports.configure({
735
756
  autoescape: false,
757
+ ...JSON.parse(nunjucksConfigs),
736
758
  tags: {
737
759
  variableStart: '\${{',
738
760
  variableEnd: '}}',
@@ -741,6 +763,7 @@ const { render, renderCompat } = (() => {
741
763
 
742
764
  const compatEnv = module.exports.configure({
743
765
  autoescape: false,
766
+ ...JSON.parse(nunjucksConfigs),
744
767
  tags: {
745
768
  variableStart: '{{',
746
769
  variableEnd: '}}',
@@ -793,8 +816,16 @@ class SecureTemplater {
793
816
  const {
794
817
  cookiecutterCompat,
795
818
  templateFilters = {},
796
- templateGlobals = {}
819
+ templateGlobals = {},
820
+ nunjucksConfigs = {}
797
821
  } = options;
822
+ const nodeVersion = getMajorNodeVersion();
823
+ if (nodeVersion >= 20 && !isNoNodeSnapshotOptionProvided()) {
824
+ throw new Error(
825
+ `When using Node.js version 20 or newer, the scaffolder backend plugin requires that it be started with the --no-node-snapshot option.
826
+ Please make sure that you have NODE_OPTIONS=--no-node-snapshot in your environment.`
827
+ );
828
+ }
798
829
  const isolate = new isolatedVm.Isolate({ memoryLimit: 128 });
799
830
  const context = await isolate.createContext();
800
831
  const contextGlobal = context.global;
@@ -808,6 +839,7 @@ class SecureTemplater {
808
839
  const nunjucksScript = await isolate.compileScript(
809
840
  mkScript(nunjucksSource)
810
841
  );
842
+ await contextGlobal.set("nunjucksConfigs", JSON.stringify(nunjucksConfigs));
811
843
  const availableFilters = Object.keys(templateFilters);
812
844
  await contextGlobal.set(
813
845
  "availableTemplateFilters",
@@ -884,7 +916,7 @@ const createDefaultFilters = ({
884
916
 
885
917
  const examples$2 = [
886
918
  {
887
- description: "Downloads a skelaton directory that lives alongside the template file and fill it out with values.",
919
+ description: "Downloads a skeleton directory that lives alongside the template file and fill it out with values.",
888
920
  example: yaml__default["default"].stringify({
889
921
  steps: [
890
922
  {
@@ -969,6 +1001,11 @@ function createFetchTemplateAction(options) {
969
1001
  title: "Replace files",
970
1002
  description: "If set, replace files in targetPath instead of skipping existing ones.",
971
1003
  type: "boolean"
1004
+ },
1005
+ token: {
1006
+ title: "Token",
1007
+ description: "An optional token to use for authentication when reading the resources.",
1008
+ type: "string"
972
1009
  }
973
1010
  }
974
1011
  }
@@ -1020,7 +1057,8 @@ function createFetchTemplateAction(options) {
1020
1057
  integrations,
1021
1058
  baseUrl: (_b = ctx.templateInfo) == null ? void 0 : _b.baseUrl,
1022
1059
  fetchUrl: ctx.input.url,
1023
- outputPath: templateDir
1060
+ outputPath: templateDir,
1061
+ token: ctx.input.token
1024
1062
  });
1025
1063
  ctx.logger.info("Listing files and directories in template");
1026
1064
  const allEntriesInTemplate = await globby__default["default"](`**/*`, {
@@ -1053,7 +1091,11 @@ function createFetchTemplateAction(options) {
1053
1091
  ...defaultTemplateFilters,
1054
1092
  ...additionalTemplateFilters
1055
1093
  },
1056
- templateGlobals: additionalTemplateGlobals
1094
+ templateGlobals: additionalTemplateGlobals,
1095
+ nunjucksConfigs: {
1096
+ trimBlocks: ctx.input.trimBlocks,
1097
+ lstripBlocks: ctx.input.lstripBlocks
1098
+ }
1057
1099
  });
1058
1100
  for (const location of allEntriesInTemplate) {
1059
1101
  let renderContents;
@@ -1419,6 +1461,36 @@ class TemplateActionRegistry {
1419
1461
  }
1420
1462
  }
1421
1463
 
1464
+ const trimEventsTillLastRecovery = (events) => {
1465
+ const recoveredEventInd = events.slice().reverse().findIndex((event) => event.type === "recovered");
1466
+ if (recoveredEventInd >= 0) {
1467
+ const ind = events.length - recoveredEventInd - 1;
1468
+ const { recoverStrategy } = events[ind].body;
1469
+ if (recoverStrategy === "startOver") {
1470
+ return {
1471
+ events: recoveredEventInd === 0 ? [] : events.slice(ind)
1472
+ };
1473
+ }
1474
+ }
1475
+ return { events };
1476
+ };
1477
+
1478
+ const intervalFromNowTill = (timeoutS, knex) => {
1479
+ let heartbeatInterval = knex.raw(`? - interval '${timeoutS} seconds'`, [
1480
+ knex.fn.now()
1481
+ ]);
1482
+ if (knex.client.config.client.includes("mysql")) {
1483
+ heartbeatInterval = knex.raw(
1484
+ `date_sub(now(), interval ${timeoutS} second)`
1485
+ );
1486
+ } else if (knex.client.config.client.includes("sqlite3")) {
1487
+ heartbeatInterval = knex.raw(`datetime('now', ?)`, [
1488
+ `-${timeoutS} seconds`
1489
+ ]);
1490
+ }
1491
+ return heartbeatInterval;
1492
+ };
1493
+
1422
1494
  var __defProp$3 = Object.defineProperty;
1423
1495
  var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1424
1496
  var __publicField$3 = (obj, key, value) => {
@@ -1455,6 +1527,28 @@ class DatabaseTaskStore {
1455
1527
  await this.runMigrations(database, client);
1456
1528
  return new DatabaseTaskStore(client);
1457
1529
  }
1530
+ isRecoverableTask(spec) {
1531
+ var _a, _b;
1532
+ return ["startOver"].includes(
1533
+ (_b = (_a = spec.EXPERIMENTAL_recovery) == null ? void 0 : _a.EXPERIMENTAL_strategy) != null ? _b : "none"
1534
+ );
1535
+ }
1536
+ parseSpec({ spec, id }) {
1537
+ try {
1538
+ return JSON.parse(spec);
1539
+ } catch (error) {
1540
+ throw new Error(`Failed to parse spec of task '${id}', ${error}`);
1541
+ }
1542
+ }
1543
+ parseTaskSecrets(taskRow) {
1544
+ try {
1545
+ return taskRow.secrets ? JSON.parse(taskRow.secrets) : void 0;
1546
+ } catch (error) {
1547
+ throw new Error(
1548
+ `Failed to parse secrets of task '${taskRow.id}', ${error}`
1549
+ );
1550
+ }
1551
+ }
1458
1552
  static async getClient(database) {
1459
1553
  if (isPluginDatabaseManager(database)) {
1460
1554
  return database.getClient();
@@ -1539,30 +1633,26 @@ class DatabaseTaskStore {
1539
1633
  if (!task) {
1540
1634
  return void 0;
1541
1635
  }
1636
+ const spec = this.parseSpec(task);
1542
1637
  const updateCount = await tx("tasks").where({ id: task.id, status: "open" }).update({
1543
1638
  status: "processing",
1544
1639
  last_heartbeat_at: this.db.fn.now(),
1545
- // remove the secrets when moving to processing state.
1546
- secrets: null
1640
+ // remove the secrets for non-recoverable tasks when moving to processing state.
1641
+ secrets: this.isRecoverableTask(spec) ? task.secrets : null
1547
1642
  });
1548
1643
  if (updateCount < 1) {
1549
1644
  return void 0;
1550
1645
  }
1551
- try {
1552
- const spec = JSON.parse(task.spec);
1553
- const secrets = task.secrets ? JSON.parse(task.secrets) : void 0;
1554
- return {
1555
- id: task.id,
1556
- spec,
1557
- status: "processing",
1558
- lastHeartbeatAt: task.last_heartbeat_at,
1559
- createdAt: task.created_at,
1560
- createdBy: (_a = task.created_by) != null ? _a : void 0,
1561
- secrets
1562
- };
1563
- } catch (error) {
1564
- throw new Error(`Failed to parse spec of task '${task.id}', ${error}`);
1565
- }
1646
+ const secrets = this.parseTaskSecrets(task);
1647
+ return {
1648
+ id: task.id,
1649
+ spec,
1650
+ status: "processing",
1651
+ lastHeartbeatAt: task.last_heartbeat_at,
1652
+ createdAt: task.created_at,
1653
+ createdBy: (_a = task.created_by) != null ? _a : void 0,
1654
+ secrets
1655
+ };
1566
1656
  });
1567
1657
  }
1568
1658
  async heartbeatTask(taskId) {
@@ -1575,20 +1665,10 @@ class DatabaseTaskStore {
1575
1665
  }
1576
1666
  async listStaleTasks(options) {
1577
1667
  const { timeoutS } = options;
1578
- let heartbeatInterval = this.db.raw(`? - interval '${timeoutS} seconds'`, [
1579
- this.db.fn.now()
1580
- ]);
1581
- if (this.db.client.config.client.includes("mysql")) {
1582
- heartbeatInterval = this.db.raw(
1583
- `date_sub(now(), interval ${timeoutS} second)`
1584
- );
1585
- } else if (this.db.client.config.client.includes("sqlite3")) {
1586
- heartbeatInterval = this.db.raw(`datetime('now', ?)`, [
1587
- `-${timeoutS} seconds`
1588
- ]);
1589
- }
1668
+ const heartbeatInterval = intervalFromNowTill(timeoutS, this.db);
1590
1669
  const rawRows = await this.db("tasks").where("status", "processing").andWhere("last_heartbeat_at", "<=", heartbeatInterval);
1591
1670
  const tasks = rawRows.map((row) => ({
1671
+ recovery: JSON.parse(row.spec).EXPERIMENTAL_recovery,
1592
1672
  taskId: row.id
1593
1673
  }));
1594
1674
  return { tasks };
@@ -1609,7 +1689,8 @@ class DatabaseTaskStore {
1609
1689
  }).limit(1).select();
1610
1690
  const updateTask = async (criteria) => {
1611
1691
  const updateCount = await tx("tasks").where(criteria).update({
1612
- status
1692
+ status,
1693
+ secrets: null
1613
1694
  });
1614
1695
  if (updateCount !== 1) {
1615
1696
  throw new errors.ConflictError(
@@ -1679,7 +1760,7 @@ class DatabaseTaskStore {
1679
1760
  );
1680
1761
  }
1681
1762
  });
1682
- return { events };
1763
+ return trimEventsTillLastRecovery(events);
1683
1764
  }
1684
1765
  async shutdownTask(options) {
1685
1766
  const { taskId } = options;
@@ -1718,7 +1799,72 @@ class DatabaseTaskStore {
1718
1799
  body: serializedBody
1719
1800
  });
1720
1801
  }
1802
+ async recoverTasks(options) {
1803
+ const taskIdsToRecover = [];
1804
+ const timeoutS = luxon.Duration.fromObject(options.timeout).as("seconds");
1805
+ await this.db.transaction(async (tx) => {
1806
+ var _a, _b;
1807
+ const heartbeatInterval = intervalFromNowTill(timeoutS, this.db);
1808
+ const result = await tx("tasks").where("status", "processing").andWhere("last_heartbeat_at", "<=", heartbeatInterval).update(
1809
+ {
1810
+ status: "open",
1811
+ last_heartbeat_at: this.db.fn.now()
1812
+ },
1813
+ ["id", "spec"]
1814
+ );
1815
+ taskIdsToRecover.push(...result.map((i) => i.id));
1816
+ for (const { id, spec } of result) {
1817
+ const taskSpec = JSON.parse(spec);
1818
+ await this.db("task_events").insert({
1819
+ task_id: id,
1820
+ event_type: "recovered",
1821
+ body: JSON.stringify({
1822
+ recoverStrategy: (_b = (_a = taskSpec.EXPERIMENTAL_recovery) == null ? void 0 : _a.EXPERIMENTAL_strategy) != null ? _b : "none"
1823
+ })
1824
+ });
1825
+ }
1826
+ });
1827
+ return { ids: taskIdsToRecover };
1828
+ }
1829
+ }
1830
+
1831
+ function isTruthy(value) {
1832
+ return lodash.isArray(value) ? value.length > 0 : !!value;
1833
+ }
1834
+ function generateExampleOutput(schema) {
1835
+ var _a, _b;
1836
+ const { examples } = schema;
1837
+ if (examples && Array.isArray(examples)) {
1838
+ return examples[0];
1839
+ }
1840
+ if (schema.type === "object") {
1841
+ return Object.fromEntries(
1842
+ Object.entries((_a = schema.properties) != null ? _a : {}).map(([key, value]) => [
1843
+ key,
1844
+ generateExampleOutput(value)
1845
+ ])
1846
+ );
1847
+ } else if (schema.type === "array") {
1848
+ const [firstSchema] = (_b = [schema.items]) == null ? void 0 : _b.flat();
1849
+ if (firstSchema) {
1850
+ return [generateExampleOutput(firstSchema)];
1851
+ }
1852
+ return [];
1853
+ } else if (schema.type === "string") {
1854
+ return "<example>";
1855
+ } else if (schema.type === "number") {
1856
+ return 0;
1857
+ } else if (schema.type === "boolean") {
1858
+ return false;
1859
+ }
1860
+ return "<unknown>";
1721
1861
  }
1862
+ const readDuration$1 = (config$1, key, defaultValue) => {
1863
+ if (config$1 == null ? void 0 : config$1.has(key)) {
1864
+ return config.readDurationFromConfig(config$1, { key });
1865
+ }
1866
+ return defaultValue;
1867
+ };
1722
1868
 
1723
1869
  var __defProp$2 = Object.defineProperty;
1724
1870
  var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -1803,9 +1949,10 @@ function defer() {
1803
1949
  return { promise, resolve };
1804
1950
  }
1805
1951
  class StorageTaskBroker {
1806
- constructor(storage, logger) {
1952
+ constructor(storage, logger, config) {
1807
1953
  this.storage = storage;
1808
1954
  this.logger = logger;
1955
+ this.config = config;
1809
1956
  __publicField$2(this, "deferredDispatch", defer());
1810
1957
  }
1811
1958
  async list(options) {
@@ -1838,6 +1985,26 @@ class StorageTaskBroker {
1838
1985
  }
1839
1986
  });
1840
1987
  }
1988
+ async recoverTasks() {
1989
+ var _a, _b, _c, _d;
1990
+ const enabled = (_a = this.config && this.config.getOptionalBoolean(
1991
+ "scaffolder.EXPERIMENTAL_recoverTasks"
1992
+ )) != null ? _a : false;
1993
+ if (enabled) {
1994
+ const defaultTimeout = { seconds: 30 };
1995
+ const timeout = readDuration$1(
1996
+ this.config,
1997
+ "scaffolder.EXPERIMENTAL_recoverTasksTimeout",
1998
+ defaultTimeout
1999
+ );
2000
+ const { ids: recoveredTaskIds } = (_d = await ((_c = (_b = this.storage).recoverTasks) == null ? void 0 : _c.call(_b, {
2001
+ timeout
2002
+ }))) != null ? _d : { ids: [] };
2003
+ if (recoveredTaskIds.length > 0) {
2004
+ this.signalDispatch();
2005
+ }
2006
+ }
2007
+ }
1841
2008
  /**
1842
2009
  * {@inheritdoc TaskBroker.claim}
1843
2010
  */
@@ -1945,38 +2112,6 @@ class StorageTaskBroker {
1945
2112
  }
1946
2113
  }
1947
2114
 
1948
- function isTruthy(value) {
1949
- return lodash.isArray(value) ? value.length > 0 : !!value;
1950
- }
1951
- function generateExampleOutput(schema) {
1952
- var _a, _b;
1953
- const { examples } = schema;
1954
- if (examples && Array.isArray(examples)) {
1955
- return examples[0];
1956
- }
1957
- if (schema.type === "object") {
1958
- return Object.fromEntries(
1959
- Object.entries((_a = schema.properties) != null ? _a : {}).map(([key, value]) => [
1960
- key,
1961
- generateExampleOutput(value)
1962
- ])
1963
- );
1964
- } else if (schema.type === "array") {
1965
- const [firstSchema] = (_b = [schema.items]) == null ? void 0 : _b.flat();
1966
- if (firstSchema) {
1967
- return [generateExampleOutput(firstSchema)];
1968
- }
1969
- return [];
1970
- } else if (schema.type === "string") {
1971
- return "<example>";
1972
- } else if (schema.type === "number") {
1973
- return 0;
1974
- } else if (schema.type === "boolean") {
1975
- return false;
1976
- }
1977
- return "<unknown>";
1978
- }
1979
-
1980
2115
  function createCounterMetric(config) {
1981
2116
  let metric = promClient.register.getSingleMetric(config.name);
1982
2117
  if (!metric) {
@@ -2510,6 +2645,10 @@ class TaskWorker {
2510
2645
  constructor(options) {
2511
2646
  this.options = options;
2512
2647
  __publicField(this, "taskQueue");
2648
+ __publicField(this, "logger");
2649
+ __publicField(this, "stopWorkers");
2650
+ this.stopWorkers = false;
2651
+ this.logger = options.logger;
2513
2652
  this.taskQueue = new PQueue__default["default"]({
2514
2653
  concurrency: options.concurrentTasksLimit
2515
2654
  });
@@ -2543,15 +2682,34 @@ class TaskWorker {
2543
2682
  permissions
2544
2683
  });
2545
2684
  }
2685
+ async recoverTasks() {
2686
+ var _a, _b, _c;
2687
+ try {
2688
+ await ((_b = (_a = this.options.taskBroker).recoverTasks) == null ? void 0 : _b.call(_a));
2689
+ } catch (err) {
2690
+ (_c = this.logger) == null ? void 0 : _c.error(errors.stringifyError(err));
2691
+ }
2692
+ }
2546
2693
  start() {
2547
2694
  (async () => {
2548
- for (; ; ) {
2695
+ while (!this.stopWorkers) {
2696
+ await new Promise((resolve) => setTimeout(resolve, 1e4));
2697
+ await this.recoverTasks();
2698
+ }
2699
+ })();
2700
+ (async () => {
2701
+ while (!this.stopWorkers) {
2549
2702
  await this.onReadyToClaimTask();
2550
- const task = await this.options.taskBroker.claim();
2551
- this.taskQueue.add(() => this.runOneTask(task));
2703
+ if (!this.stopWorkers) {
2704
+ const task = await this.options.taskBroker.claim();
2705
+ void this.taskQueue.add(() => this.runOneTask(task));
2706
+ }
2552
2707
  }
2553
2708
  })();
2554
2709
  }
2710
+ stop() {
2711
+ this.stopWorkers = true;
2712
+ }
2555
2713
  onReadyToClaimTask() {
2556
2714
  if (this.taskQueue.pending < this.options.concurrentTasksLimit) {
2557
2715
  return Promise.resolve();
@@ -2812,7 +2970,7 @@ async function createRouter(options) {
2812
2970
  let taskBroker;
2813
2971
  if (!options.taskBroker) {
2814
2972
  const databaseTaskStore = await DatabaseTaskStore.create({ database });
2815
- taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
2973
+ taskBroker = new StorageTaskBroker(databaseTaskStore, logger, config);
2816
2974
  if (scheduler && databaseTaskStore.listStaleTasks) {
2817
2975
  await scheduler.scheduleTask({
2818
2976
  id: "close_stale_tasks",
@@ -2869,7 +3027,16 @@ async function createRouter(options) {
2869
3027
  additionalTemplateGlobals
2870
3028
  });
2871
3029
  actionsToRegister.forEach((action) => actionRegistry.register(action));
2872
- workers.forEach((worker) => worker.start());
3030
+ const launchWorkers = () => workers.forEach((worker) => worker.start());
3031
+ const shutdownWorkers = () => {
3032
+ workers.forEach((worker) => worker.stop());
3033
+ };
3034
+ if (options.lifecycle) {
3035
+ options.lifecycle.addStartupHook(launchWorkers);
3036
+ options.lifecycle.addShutdownHook(shutdownWorkers);
3037
+ } else {
3038
+ launchWorkers();
3039
+ }
2873
3040
  const dryRunner = createDryRunner({
2874
3041
  actionRegistry,
2875
3042
  integrations,
@@ -2983,6 +3150,7 @@ async function createRouter(options) {
2983
3150
  name: (_b2 = step.name) != null ? _b2 : step.action
2984
3151
  };
2985
3152
  }),
3153
+ EXPERIMENTAL_recovery: template.spec.EXPERIMENTAL_recovery,
2986
3154
  output: (_b = template.spec.output) != null ? _b : {},
2987
3155
  parameters: values,
2988
3156
  user: {
@@ -3214,4 +3382,4 @@ exports.createRouter = createRouter;
3214
3382
  exports.createWaitAction = createWaitAction;
3215
3383
  exports.scaffolderActionRules = scaffolderActionRules;
3216
3384
  exports.scaffolderTemplateRules = scaffolderTemplateRules;
3217
- //# sourceMappingURL=router-e667d04e.cjs.js.map
3385
+ //# sourceMappingURL=router-43c4dd8a.cjs.js.map