@backstage/plugin-scaffolder-backend 4.0.3-next.1 → 4.0.3
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/CHANGELOG.md +28 -0
- package/dist/ScaffolderPlugin.cjs.js +1 -1
- package/dist/scaffolder/actions/builtin/fetch/template.cjs.js +3 -1
- package/dist/scaffolder/actions/builtin/fetch/template.cjs.js.map +1 -1
- package/dist/scaffolder/actions/builtin/fetch/templateActionHandler.cjs.js +70 -68
- package/dist/scaffolder/actions/builtin/fetch/templateActionHandler.cjs.js.map +1 -1
- package/dist/scaffolder/actions/builtin/fetch/templateFile.cjs.js +4 -1
- package/dist/scaffolder/actions/builtin/fetch/templateFile.cjs.js.map +1 -1
- package/dist/scaffolder/actions/builtin/fetch/templateFileActionHandler.cjs.js +14 -30
- package/dist/scaffolder/actions/builtin/fetch/templateFileActionHandler.cjs.js.map +1 -1
- package/dist/scaffolder/dryrun/createDryRunner.cjs.js +6 -0
- package/dist/scaffolder/dryrun/createDryRunner.cjs.js.map +1 -1
- package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js +103 -109
- package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js.map +1 -1
- package/dist/scaffolder/tasks/TaskWorker.cjs.js +5 -2
- package/dist/scaffolder/tasks/TaskWorker.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +1 -1
- package/dist/util/templating.cjs.js +12 -0
- package/dist/util/templating.cjs.js.map +1 -1
- package/package.json +21 -26
- package/assets/nunjucks.js.txt +0 -10385
- package/dist/lib/templating/SecureTemplater.cjs.js +0 -188
- package/dist/lib/templating/SecureTemplater.cjs.js.map +0 -1
- package/dist/lib/templating/helpers.cjs.js +0 -13
- package/dist/lib/templating/helpers.cjs.js.map +0 -1
|
@@ -4,9 +4,8 @@ var errors = require('@backstage/errors');
|
|
|
4
4
|
var fs = require('fs-extra');
|
|
5
5
|
var jsonschema = require('jsonschema');
|
|
6
6
|
var path = require('node:path');
|
|
7
|
-
var
|
|
7
|
+
var nunjitsu = require('nunjitsu');
|
|
8
8
|
var winston = require('winston');
|
|
9
|
-
var SecureTemplater = require('../../lib/templating/SecureTemplater.cjs.js');
|
|
10
9
|
var helper = require('./helper.cjs.js');
|
|
11
10
|
var pluginPermissionCommon = require('@backstage/plugin-permission-common');
|
|
12
11
|
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
@@ -40,12 +39,22 @@ function _interopNamespaceCompat(e) {
|
|
|
40
39
|
|
|
41
40
|
var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
42
41
|
var path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
43
|
-
var nunjucks__default = /*#__PURE__*/_interopDefaultCompat(nunjucks);
|
|
44
42
|
var winston__namespace = /*#__PURE__*/_interopNamespaceCompat(winston);
|
|
45
43
|
|
|
46
44
|
const isValidTaskSpec = (taskSpec) => {
|
|
47
45
|
return taskSpec.apiVersion === "scaffolder.backstage.io/v1beta3";
|
|
48
46
|
};
|
|
47
|
+
function stringifyEachLogValue(value) {
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return value.map(
|
|
50
|
+
(item) => item === null || item === void 0 ? "" : stringifyEachLogValue(item)
|
|
51
|
+
).join(",");
|
|
52
|
+
}
|
|
53
|
+
if (value !== null && typeof value === "object") {
|
|
54
|
+
return "[object Object]";
|
|
55
|
+
}
|
|
56
|
+
return String(value);
|
|
57
|
+
}
|
|
49
58
|
const createStepLogger = ({
|
|
50
59
|
task,
|
|
51
60
|
step,
|
|
@@ -96,17 +105,19 @@ const isActionAuthorized = pluginPermissionNode.createConditionAuthorizer(
|
|
|
96
105
|
Object.values(rules.scaffolderActionRules)
|
|
97
106
|
);
|
|
98
107
|
class NunjucksWorkflowRunner {
|
|
99
|
-
|
|
108
|
+
defaultTemplateCapabilities;
|
|
100
109
|
options;
|
|
101
110
|
environment = { parameters: {}, secrets: {} };
|
|
102
111
|
tracker;
|
|
103
112
|
constructor(options) {
|
|
104
113
|
this.options = options;
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
this.defaultTemplateCapabilities = templating.collectTemplateCapabilities({
|
|
115
|
+
filters: templating.convertFiltersToRecord(
|
|
116
|
+
createDefaultFilters.createDefaultFilters({
|
|
117
|
+
integrations: this.options.integrations
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
});
|
|
110
121
|
this.tracker = scaffoldingTracker(options.metrics);
|
|
111
122
|
}
|
|
112
123
|
async getEnvironmentConfig() {
|
|
@@ -122,47 +133,11 @@ class NunjucksWorkflowRunner {
|
|
|
122
133
|
secrets: {}
|
|
123
134
|
};
|
|
124
135
|
}
|
|
125
|
-
|
|
126
|
-
const { parser, nodes } = nunjucks__default.default;
|
|
127
|
-
const parsed = parser.parse(
|
|
128
|
-
input,
|
|
129
|
-
{},
|
|
130
|
-
{
|
|
131
|
-
autoescape: false,
|
|
132
|
-
tags: {
|
|
133
|
-
variableStart: "${{",
|
|
134
|
-
variableEnd: "}}"
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
);
|
|
138
|
-
return parsed.children.length === 1 && !(parsed.children[0]?.children?.[0] instanceof nodes.TemplateData);
|
|
139
|
-
}
|
|
140
|
-
render(input, context, renderTemplate) {
|
|
136
|
+
render(input, context, templateRenderer) {
|
|
141
137
|
return JSON.parse(JSON.stringify(input), (_key, value) => {
|
|
142
138
|
try {
|
|
143
139
|
if (typeof value === "string") {
|
|
144
|
-
|
|
145
|
-
if (this.isSingleTemplateString(value)) {
|
|
146
|
-
const wrappedDumped = value.replace(
|
|
147
|
-
/\${{(.+)}}/g,
|
|
148
|
-
"${{ ( $1 ) | dump }}"
|
|
149
|
-
);
|
|
150
|
-
const templated2 = renderTemplate(wrappedDumped, context);
|
|
151
|
-
if (templated2 === "") {
|
|
152
|
-
return void 0;
|
|
153
|
-
}
|
|
154
|
-
return JSON.parse(templated2);
|
|
155
|
-
}
|
|
156
|
-
} catch (ex) {
|
|
157
|
-
this.options.logger.error(
|
|
158
|
-
`Failed to parse template string: ${value} with error ${ex.message}`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
const templated = renderTemplate(value, context);
|
|
162
|
-
if (templated === "") {
|
|
163
|
-
return void 0;
|
|
164
|
-
}
|
|
165
|
-
return templated;
|
|
140
|
+
return templateRenderer.renderValue(value, context);
|
|
166
141
|
}
|
|
167
142
|
} catch {
|
|
168
143
|
return value;
|
|
@@ -170,7 +145,7 @@ class NunjucksWorkflowRunner {
|
|
|
170
145
|
return value;
|
|
171
146
|
});
|
|
172
147
|
}
|
|
173
|
-
async executeStep(task, step, context,
|
|
148
|
+
async executeStep(task, step, context, preparedContext, templateRenderer, taskTrack, workspacePath, decision) {
|
|
174
149
|
const stepTrack = await this.tracker.stepStart(task, step);
|
|
175
150
|
if (task.cancelSignal.aborted) {
|
|
176
151
|
throw new Error(
|
|
@@ -178,9 +153,9 @@ class NunjucksWorkflowRunner {
|
|
|
178
153
|
);
|
|
179
154
|
}
|
|
180
155
|
try {
|
|
181
|
-
if (step.if === false || typeof step.if === "string" && step.each === void 0 && !helper.isTruthy(this.render(step.if,
|
|
156
|
+
if (step.if === false || typeof step.if === "string" && step.each === void 0 && !helper.isTruthy(this.render(step.if, preparedContext, templateRenderer))) {
|
|
182
157
|
await stepTrack.skipFalsy();
|
|
183
|
-
return;
|
|
158
|
+
return preparedContext;
|
|
184
159
|
}
|
|
185
160
|
const action = await this.options.actionRegistry.get(step.action, {
|
|
186
161
|
credentials: await task.getInitiatorCredentials()
|
|
@@ -204,18 +179,11 @@ class NunjucksWorkflowRunner {
|
|
|
204
179
|
"***"
|
|
205
180
|
])
|
|
206
181
|
);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
parameters: this.environment?.parameters || {},
|
|
213
|
-
secrets: redactedEnvironmentSecrets
|
|
214
|
-
},
|
|
215
|
-
secrets: redactedSecrets
|
|
216
|
-
},
|
|
217
|
-
renderTemplate
|
|
218
|
-
)) ?? {};
|
|
182
|
+
const dryRunContext = preparedContext.withValue(["environment"], {
|
|
183
|
+
parameters: this.environment?.parameters || {},
|
|
184
|
+
secrets: redactedEnvironmentSecrets
|
|
185
|
+
}).withValue(["secrets"], redactedSecrets);
|
|
186
|
+
const debugInput = (step.input && this.render(step.input, dryRunContext, templateRenderer)) ?? {};
|
|
219
187
|
taskLogger.info(
|
|
220
188
|
`Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
|
|
221
189
|
debugInput,
|
|
@@ -233,7 +201,10 @@ class NunjucksWorkflowRunner {
|
|
|
233
201
|
} else {
|
|
234
202
|
context.steps[step.id] = { output: {} };
|
|
235
203
|
}
|
|
236
|
-
return
|
|
204
|
+
return preparedContext.withValue(
|
|
205
|
+
["steps", step.id],
|
|
206
|
+
context.steps[step.id]
|
|
207
|
+
);
|
|
237
208
|
}
|
|
238
209
|
}
|
|
239
210
|
const preIterationContext = {
|
|
@@ -244,7 +215,14 @@ class NunjucksWorkflowRunner {
|
|
|
244
215
|
},
|
|
245
216
|
secrets: task.secrets ?? {}
|
|
246
217
|
};
|
|
247
|
-
const
|
|
218
|
+
const preparedPreIterationContext = preparedContext.withValue(
|
|
219
|
+
["environment"],
|
|
220
|
+
preIterationContext.environment
|
|
221
|
+
).withValue(
|
|
222
|
+
["secrets"],
|
|
223
|
+
preIterationContext.secrets
|
|
224
|
+
);
|
|
225
|
+
const resolvedEach = step.each && this.render(step.each, preparedPreIterationContext, templateRenderer);
|
|
248
226
|
if (step.each && !resolvedEach) {
|
|
249
227
|
throw new errors.InputError(
|
|
250
228
|
`Invalid value on action ${action.id}.each parameter, "${step.each}" cannot be resolved to a value`
|
|
@@ -253,13 +231,17 @@ class NunjucksWorkflowRunner {
|
|
|
253
231
|
const iterations = (resolvedEach ? Object.entries(resolvedEach).map(([key, value]) => ({
|
|
254
232
|
each: { key, value }
|
|
255
233
|
})) : [{}]).map((i) => {
|
|
256
|
-
const
|
|
257
|
-
|
|
234
|
+
const preparedFullContext = "each" in i ? preparedPreIterationContext.withValue(
|
|
235
|
+
["each"],
|
|
236
|
+
i.each
|
|
237
|
+
) : preparedPreIterationContext;
|
|
238
|
+
const shouldRun = !("each" in i) || !step.if || helper.isTruthy(this.render(step.if, preparedFullContext, templateRenderer));
|
|
258
239
|
return {
|
|
259
240
|
...i,
|
|
241
|
+
preparedContext: preparedFullContext,
|
|
260
242
|
shouldRun,
|
|
261
243
|
// Secrets are only passed when templating the input to actions for security reasons
|
|
262
|
-
input: step.input ? this.render(step.input,
|
|
244
|
+
input: step.input ? this.render(step.input, preparedFullContext, templateRenderer) : {}
|
|
263
245
|
};
|
|
264
246
|
});
|
|
265
247
|
for (const iteration of iterations) {
|
|
@@ -301,7 +283,7 @@ class NunjucksWorkflowRunner {
|
|
|
301
283
|
taskLogger.info(
|
|
302
284
|
`Skipping step each: ${JSON.stringify(
|
|
303
285
|
iteration.each,
|
|
304
|
-
(
|
|
286
|
+
(key, value) => key ? stringifyEachLogValue(value) : value,
|
|
305
287
|
0
|
|
306
288
|
)}`
|
|
307
289
|
);
|
|
@@ -310,7 +292,7 @@ class NunjucksWorkflowRunner {
|
|
|
310
292
|
taskLogger.info(
|
|
311
293
|
`Running step each: ${JSON.stringify(
|
|
312
294
|
iteration.each,
|
|
313
|
-
(
|
|
295
|
+
(key, value) => key ? stringifyEachLogValue(value) : value,
|
|
314
296
|
0
|
|
315
297
|
)}`
|
|
316
298
|
);
|
|
@@ -319,19 +301,11 @@ class NunjucksWorkflowRunner {
|
|
|
319
301
|
const hasSecrets = Object.keys(task.secrets ?? {}).length > 0 || Object.keys(this.environment?.secrets ?? {}).length > 0;
|
|
320
302
|
if (hasSecrets) {
|
|
321
303
|
try {
|
|
322
|
-
const
|
|
323
|
-
...preIterationContext,
|
|
324
|
-
...iteration.each ? { each: iteration.each } : {},
|
|
325
|
-
secrets: {},
|
|
326
|
-
environment: {
|
|
327
|
-
...preIterationContext.environment,
|
|
328
|
-
secrets: {}
|
|
329
|
-
}
|
|
330
|
-
};
|
|
304
|
+
const preparedContextNoSecrets = iteration.preparedContext.withValue(["secrets"], {}).withValue(["environment", "secrets"], {});
|
|
331
305
|
const inputWithoutSecrets = this.render(
|
|
332
306
|
step.input,
|
|
333
|
-
|
|
334
|
-
|
|
307
|
+
preparedContextNoSecrets,
|
|
308
|
+
templateRenderer
|
|
335
309
|
);
|
|
336
310
|
taskLogger.addRedactions(
|
|
337
311
|
collectSecretRedactions(iteration.input, inputWithoutSecrets)
|
|
@@ -354,28 +328,38 @@ class NunjucksWorkflowRunner {
|
|
|
354
328
|
const key = `v1.task.checkpoint.${step.id}.${checkpointKey}`;
|
|
355
329
|
try {
|
|
356
330
|
let prevValue;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
|
|
331
|
+
let value;
|
|
332
|
+
try {
|
|
333
|
+
if (prevTaskState) {
|
|
334
|
+
const prevState = prevTaskState.state?.checkpoints?.[key];
|
|
335
|
+
if (prevState && prevState.status === "success") {
|
|
336
|
+
prevValue = prevState.value;
|
|
337
|
+
}
|
|
361
338
|
}
|
|
339
|
+
value = prevValue !== void 0 ? prevValue : await fn();
|
|
340
|
+
} catch (err) {
|
|
341
|
+
try {
|
|
342
|
+
await task.updateCheckpoint?.({
|
|
343
|
+
key,
|
|
344
|
+
status: "failed",
|
|
345
|
+
reason: errors.stringifyError(err)
|
|
346
|
+
});
|
|
347
|
+
} catch (persistenceError) {
|
|
348
|
+
throw new AggregateError(
|
|
349
|
+
[err, persistenceError],
|
|
350
|
+
`Checkpoint '${checkpointKey}' failed and its failure state could not be persisted`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
throw err;
|
|
362
354
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
task.updateCheckpoint?.({
|
|
355
|
+
if (prevValue === void 0) {
|
|
356
|
+
await task.updateCheckpoint?.({
|
|
366
357
|
key,
|
|
367
358
|
status: "success",
|
|
368
359
|
value: value ?? {}
|
|
369
360
|
});
|
|
370
361
|
}
|
|
371
362
|
return value;
|
|
372
|
-
} catch (err) {
|
|
373
|
-
task.updateCheckpoint?.({
|
|
374
|
-
key,
|
|
375
|
-
status: "failed",
|
|
376
|
-
reason: errors.stringifyError(err)
|
|
377
|
-
});
|
|
378
|
-
throw err;
|
|
379
363
|
} finally {
|
|
380
364
|
await task.serializeWorkspace?.({ path: workspacePath });
|
|
381
365
|
}
|
|
@@ -410,12 +394,17 @@ class NunjucksWorkflowRunner {
|
|
|
410
394
|
await fs__default.default.remove(tmpDir);
|
|
411
395
|
}
|
|
412
396
|
context.steps[step.id] = { output: stepOutput };
|
|
397
|
+
const updatedPreparedContext = preparedContext.withValue(
|
|
398
|
+
["steps", step.id],
|
|
399
|
+
context.steps[step.id]
|
|
400
|
+
);
|
|
413
401
|
if (task.cancelSignal.aborted) {
|
|
414
402
|
throw new Error(
|
|
415
403
|
`Step ${step.id} (${step.name}) of task ${task.taskId} has been cancelled.`
|
|
416
404
|
);
|
|
417
405
|
}
|
|
418
406
|
await stepTrack.markSuccessful();
|
|
407
|
+
return updatedPreparedContext;
|
|
419
408
|
} catch (err) {
|
|
420
409
|
await taskTrack.markFailed(step, err);
|
|
421
410
|
await stepTrack.markFailed();
|
|
@@ -432,17 +421,18 @@ class NunjucksWorkflowRunner {
|
|
|
432
421
|
}
|
|
433
422
|
const taskId = await task.getWorkspaceName();
|
|
434
423
|
const workspacePath = path__default.default.join(this.options.workingDirectory, taskId);
|
|
435
|
-
const {
|
|
424
|
+
const { templateCapabilities } = this.options;
|
|
436
425
|
this.environment = await this.getEnvironmentConfig();
|
|
437
426
|
const taskState = { failed: false };
|
|
438
427
|
const statusCheckInvoked = { value: false };
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
...
|
|
428
|
+
const templateRenderer = nunjitsu.createTemplateRenderer({
|
|
429
|
+
allowRegexExecution: true,
|
|
430
|
+
filters: {
|
|
431
|
+
...this.defaultTemplateCapabilities.filters,
|
|
432
|
+
...templateCapabilities?.filters
|
|
443
433
|
},
|
|
444
|
-
|
|
445
|
-
...
|
|
434
|
+
globals: {
|
|
435
|
+
...templateCapabilities?.globals,
|
|
446
436
|
always: () => {
|
|
447
437
|
statusCheckInvoked.value = true;
|
|
448
438
|
return true;
|
|
@@ -471,6 +461,9 @@ class NunjucksWorkflowRunner {
|
|
|
471
461
|
}
|
|
472
462
|
}
|
|
473
463
|
};
|
|
464
|
+
let preparedContext = templateRenderer.prepareContext(
|
|
465
|
+
context
|
|
466
|
+
);
|
|
474
467
|
const [decision] = this.options.permissions && task.spec.steps.length ? await this.options.permissions.authorizeConditional(
|
|
475
468
|
[{ permission: alpha.actionExecutePermission }],
|
|
476
469
|
{ credentials: await task.getInitiatorCredentials() }
|
|
@@ -487,7 +480,7 @@ class NunjucksWorkflowRunner {
|
|
|
487
480
|
continue;
|
|
488
481
|
}
|
|
489
482
|
statusCheckInvoked.value = false;
|
|
490
|
-
this.render(step.if,
|
|
483
|
+
this.render(step.if, preparedContext, templateRenderer);
|
|
491
484
|
if (!statusCheckInvoked.value) {
|
|
492
485
|
await task.emitLog(
|
|
493
486
|
`Skipping step ${step.id} because a previous step failed`,
|
|
@@ -497,11 +490,12 @@ class NunjucksWorkflowRunner {
|
|
|
497
490
|
}
|
|
498
491
|
}
|
|
499
492
|
try {
|
|
500
|
-
await this.executeStep(
|
|
493
|
+
preparedContext = await this.executeStep(
|
|
501
494
|
task,
|
|
502
495
|
step,
|
|
503
496
|
context,
|
|
504
|
-
|
|
497
|
+
preparedContext,
|
|
498
|
+
templateRenderer,
|
|
505
499
|
taskTrack,
|
|
506
500
|
workspacePath,
|
|
507
501
|
decision
|
|
@@ -534,7 +528,11 @@ ${error.stack}`,
|
|
|
534
528
|
}
|
|
535
529
|
throw firstError;
|
|
536
530
|
}
|
|
537
|
-
const output = this.render(
|
|
531
|
+
const output = this.render(
|
|
532
|
+
task.spec.output,
|
|
533
|
+
preparedContext,
|
|
534
|
+
templateRenderer
|
|
535
|
+
);
|
|
538
536
|
if (Array.isArray(output?.links)) {
|
|
539
537
|
output.links = helper.filterConditionalItems(output.links);
|
|
540
538
|
}
|
|
@@ -545,10 +543,6 @@ ${error.stack}`,
|
|
|
545
543
|
await task.cleanWorkspace?.();
|
|
546
544
|
return { output };
|
|
547
545
|
} finally {
|
|
548
|
-
try {
|
|
549
|
-
dispose();
|
|
550
|
-
} catch {
|
|
551
|
-
}
|
|
552
546
|
if (workspacePath) {
|
|
553
547
|
await fs__default.default.remove(workspacePath);
|
|
554
548
|
}
|