@backstage/plugin-scaffolder-backend 1.19.2-next.2 → 1.19.2-next.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/alpha/package.json +1 -1
- package/dist/alpha.cjs.js +6 -11
- package/dist/alpha.cjs.js.map +1 -1
- package/dist/cjs/router-46321f3f.cjs.js +3207 -0
- package/dist/cjs/router-46321f3f.cjs.js.map +1 -0
- package/dist/index.cjs.js +67 -29
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +61 -474
- package/package.json +17 -34
- package/dist/cjs/router-1200925e.cjs.js +0 -8191
- package/dist/cjs/router-1200925e.cjs.js.map +0 -1
|
@@ -0,0 +1,3207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
4
|
+
var config = require('@backstage/config');
|
|
5
|
+
var errors = require('@backstage/errors');
|
|
6
|
+
var integration = require('@backstage/integration');
|
|
7
|
+
var pluginScaffolderCommon = require('@backstage/plugin-scaffolder-common');
|
|
8
|
+
var alpha = require('@backstage/plugin-scaffolder-common/alpha');
|
|
9
|
+
var express = require('express');
|
|
10
|
+
var Router = require('express-promise-router');
|
|
11
|
+
var jsonschema = require('jsonschema');
|
|
12
|
+
var zod = require('zod');
|
|
13
|
+
var pluginScaffolderNode = require('@backstage/plugin-scaffolder-node');
|
|
14
|
+
var yaml = require('yaml');
|
|
15
|
+
var fs = require('fs-extra');
|
|
16
|
+
var backendCommon = require('@backstage/backend-common');
|
|
17
|
+
var path = require('path');
|
|
18
|
+
var luxon = require('luxon');
|
|
19
|
+
var globby = require('globby');
|
|
20
|
+
var isbinaryfile = require('isbinaryfile');
|
|
21
|
+
var isolatedVm = require('isolated-vm');
|
|
22
|
+
var get = require('lodash/get');
|
|
23
|
+
var github = require('@backstage/plugin-scaffolder-backend-module-github');
|
|
24
|
+
var azure = require('@backstage/plugin-scaffolder-backend-module-azure');
|
|
25
|
+
var bitbucket = require('@backstage/plugin-scaffolder-backend-module-bitbucket');
|
|
26
|
+
var gerrit = require('@backstage/plugin-scaffolder-backend-module-gerrit');
|
|
27
|
+
var gitlab = require('@backstage/plugin-scaffolder-backend-module-gitlab');
|
|
28
|
+
var uuid = require('uuid');
|
|
29
|
+
var ObservableImpl = require('zen-observable');
|
|
30
|
+
var PQueue = require('p-queue');
|
|
31
|
+
var winston = require('winston');
|
|
32
|
+
var nunjucks = require('nunjucks');
|
|
33
|
+
var stream = require('stream');
|
|
34
|
+
var lodash = require('lodash');
|
|
35
|
+
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
36
|
+
var promClient = require('prom-client');
|
|
37
|
+
var pluginPermissionCommon = require('@backstage/plugin-permission-common');
|
|
38
|
+
var url = require('url');
|
|
39
|
+
var os = require('os');
|
|
40
|
+
|
|
41
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
42
|
+
|
|
43
|
+
function _interopNamespace(e) {
|
|
44
|
+
if (e && e.__esModule) return e;
|
|
45
|
+
var n = Object.create(null);
|
|
46
|
+
if (e) {
|
|
47
|
+
Object.keys(e).forEach(function (k) {
|
|
48
|
+
if (k !== 'default') {
|
|
49
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
50
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
get: function () { return e[k]; }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
n["default"] = e;
|
|
58
|
+
return Object.freeze(n);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var express__default = /*#__PURE__*/_interopDefaultLegacy(express);
|
|
62
|
+
var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
|
|
63
|
+
var yaml__default = /*#__PURE__*/_interopDefaultLegacy(yaml);
|
|
64
|
+
var yaml__namespace = /*#__PURE__*/_interopNamespace(yaml);
|
|
65
|
+
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
66
|
+
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
67
|
+
var globby__default = /*#__PURE__*/_interopDefaultLegacy(globby);
|
|
68
|
+
var get__default = /*#__PURE__*/_interopDefaultLegacy(get);
|
|
69
|
+
var ObservableImpl__default = /*#__PURE__*/_interopDefaultLegacy(ObservableImpl);
|
|
70
|
+
var PQueue__default = /*#__PURE__*/_interopDefaultLegacy(PQueue);
|
|
71
|
+
var winston__namespace = /*#__PURE__*/_interopNamespace(winston);
|
|
72
|
+
var nunjucks__default = /*#__PURE__*/_interopDefaultLegacy(nunjucks);
|
|
73
|
+
var os__default = /*#__PURE__*/_interopDefaultLegacy(os);
|
|
74
|
+
|
|
75
|
+
const examples$9 = [
|
|
76
|
+
{
|
|
77
|
+
description: "Register with the catalog",
|
|
78
|
+
example: yaml__default["default"].stringify({
|
|
79
|
+
steps: [
|
|
80
|
+
{
|
|
81
|
+
action: "catalog:register",
|
|
82
|
+
id: "register-with-catalog",
|
|
83
|
+
name: "Register with the catalog",
|
|
84
|
+
input: {
|
|
85
|
+
catalogInfoUrl: "http://github.com/backstage/backstage/blob/master/catalog-info.yaml"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const id$4 = "catalog:register";
|
|
94
|
+
function createCatalogRegisterAction(options) {
|
|
95
|
+
const { catalogClient, integrations } = options;
|
|
96
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
97
|
+
id: id$4,
|
|
98
|
+
description: "Registers entities from a catalog descriptor file in the workspace into the software catalog.",
|
|
99
|
+
examples: examples$9,
|
|
100
|
+
schema: {
|
|
101
|
+
input: {
|
|
102
|
+
oneOf: [
|
|
103
|
+
{
|
|
104
|
+
type: "object",
|
|
105
|
+
required: ["catalogInfoUrl"],
|
|
106
|
+
properties: {
|
|
107
|
+
catalogInfoUrl: {
|
|
108
|
+
title: "Catalog Info URL",
|
|
109
|
+
description: "An absolute URL pointing to the catalog info file location",
|
|
110
|
+
type: "string"
|
|
111
|
+
},
|
|
112
|
+
optional: {
|
|
113
|
+
title: "Optional",
|
|
114
|
+
description: "Permit the registered location to optionally exist. Default: false",
|
|
115
|
+
type: "boolean"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: "object",
|
|
121
|
+
required: ["repoContentsUrl"],
|
|
122
|
+
properties: {
|
|
123
|
+
repoContentsUrl: {
|
|
124
|
+
title: "Repository Contents URL",
|
|
125
|
+
description: "An absolute URL pointing to the root of a repository directory tree",
|
|
126
|
+
type: "string"
|
|
127
|
+
},
|
|
128
|
+
catalogInfoPath: {
|
|
129
|
+
title: "Fetch URL",
|
|
130
|
+
description: "A relative path from the repo root pointing to the catalog info file, defaults to /catalog-info.yaml",
|
|
131
|
+
type: "string"
|
|
132
|
+
},
|
|
133
|
+
optional: {
|
|
134
|
+
title: "Optional",
|
|
135
|
+
description: "Permit the registered location to optionally exist. Default: false",
|
|
136
|
+
type: "boolean"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
output: {
|
|
143
|
+
type: "object",
|
|
144
|
+
required: ["catalogInfoUrl"],
|
|
145
|
+
properties: {
|
|
146
|
+
entityRef: {
|
|
147
|
+
type: "string"
|
|
148
|
+
},
|
|
149
|
+
catalogInfoUrl: {
|
|
150
|
+
type: "string"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
async handler(ctx) {
|
|
156
|
+
var _a, _b;
|
|
157
|
+
const { input } = ctx;
|
|
158
|
+
let catalogInfoUrl;
|
|
159
|
+
if ("catalogInfoUrl" in input) {
|
|
160
|
+
catalogInfoUrl = input.catalogInfoUrl;
|
|
161
|
+
} else {
|
|
162
|
+
const { repoContentsUrl, catalogInfoPath = "/catalog-info.yaml" } = input;
|
|
163
|
+
const integration = integrations.byUrl(repoContentsUrl);
|
|
164
|
+
if (!integration) {
|
|
165
|
+
throw new errors.InputError(
|
|
166
|
+
`No integration found for host ${repoContentsUrl}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
catalogInfoUrl = integration.resolveUrl({
|
|
170
|
+
base: repoContentsUrl,
|
|
171
|
+
url: catalogInfoPath
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
ctx.logger.info(`Registering ${catalogInfoUrl} in the catalog`);
|
|
175
|
+
try {
|
|
176
|
+
await catalogClient.addLocation(
|
|
177
|
+
{
|
|
178
|
+
type: "url",
|
|
179
|
+
target: catalogInfoUrl
|
|
180
|
+
},
|
|
181
|
+
((_a = ctx.secrets) == null ? void 0 : _a.backstageToken) ? { token: ctx.secrets.backstageToken } : {}
|
|
182
|
+
);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (!input.optional) {
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const result = await catalogClient.addLocation(
|
|
190
|
+
{
|
|
191
|
+
dryRun: true,
|
|
192
|
+
type: "url",
|
|
193
|
+
target: catalogInfoUrl
|
|
194
|
+
},
|
|
195
|
+
((_b = ctx.secrets) == null ? void 0 : _b.backstageToken) ? { token: ctx.secrets.backstageToken } : {}
|
|
196
|
+
);
|
|
197
|
+
if (result.entities.length) {
|
|
198
|
+
const { entities } = result;
|
|
199
|
+
let entity;
|
|
200
|
+
entity = entities.find(
|
|
201
|
+
(e) => !e.metadata.name.startsWith("generated-") && e.kind === "Component"
|
|
202
|
+
);
|
|
203
|
+
if (!entity) {
|
|
204
|
+
entity = entities.find(
|
|
205
|
+
(e) => !e.metadata.name.startsWith("generated-")
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (!entity) {
|
|
209
|
+
entity = entities[0];
|
|
210
|
+
}
|
|
211
|
+
ctx.output("entityRef", catalogModel.stringifyEntityRef(entity));
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
if (!input.optional) {
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
ctx.output("catalogInfoUrl", catalogInfoUrl);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const examples$8 = [
|
|
224
|
+
{
|
|
225
|
+
description: "Write a catalog yaml file",
|
|
226
|
+
example: yaml__namespace.stringify({
|
|
227
|
+
steps: [
|
|
228
|
+
{
|
|
229
|
+
action: "catalog:write",
|
|
230
|
+
id: "create-catalog-info-file",
|
|
231
|
+
name: "Create catalog file",
|
|
232
|
+
input: {
|
|
233
|
+
entity: {
|
|
234
|
+
apiVersion: "backstage.io/v1alpha1",
|
|
235
|
+
kind: "Component",
|
|
236
|
+
metadata: {
|
|
237
|
+
name: "test",
|
|
238
|
+
annotations: {}
|
|
239
|
+
},
|
|
240
|
+
spec: {
|
|
241
|
+
type: "service",
|
|
242
|
+
lifecycle: "production",
|
|
243
|
+
owner: "default/owner"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
const id$3 = "catalog:write";
|
|
254
|
+
function createCatalogWriteAction() {
|
|
255
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
256
|
+
id: id$3,
|
|
257
|
+
description: "Writes the catalog-info.yaml for your template",
|
|
258
|
+
schema: {
|
|
259
|
+
input: zod.z.object({
|
|
260
|
+
filePath: zod.z.string().optional().describe("Defaults to catalog-info.yaml"),
|
|
261
|
+
// TODO: this should reference an zod entity validator if it existed.
|
|
262
|
+
entity: zod.z.record(zod.z.any()).describe(
|
|
263
|
+
"You can provide the same values used in the Entity schema."
|
|
264
|
+
)
|
|
265
|
+
})
|
|
266
|
+
},
|
|
267
|
+
examples: examples$8,
|
|
268
|
+
supportsDryRun: true,
|
|
269
|
+
async handler(ctx) {
|
|
270
|
+
ctx.logStream.write(`Writing catalog-info.yaml`);
|
|
271
|
+
const { filePath, entity } = ctx.input;
|
|
272
|
+
const path = filePath != null ? filePath : "catalog-info.yaml";
|
|
273
|
+
await fs__default["default"].writeFile(
|
|
274
|
+
backendCommon.resolveSafeChildPath(ctx.workspacePath, path),
|
|
275
|
+
yaml__namespace.stringify(entity)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const examples$7 = [
|
|
282
|
+
{
|
|
283
|
+
description: "Fetch entity by reference",
|
|
284
|
+
example: yaml__default["default"].stringify({
|
|
285
|
+
steps: [
|
|
286
|
+
{
|
|
287
|
+
action: "catalog:fetch",
|
|
288
|
+
id: "fetch",
|
|
289
|
+
name: "Fetch catalog entity",
|
|
290
|
+
input: {
|
|
291
|
+
entityRef: "component:default/name"
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
]
|
|
295
|
+
})
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
description: "Fetch multiple entities by reference",
|
|
299
|
+
example: yaml__default["default"].stringify({
|
|
300
|
+
steps: [
|
|
301
|
+
{
|
|
302
|
+
action: "catalog:fetch",
|
|
303
|
+
id: "fetchMultiple",
|
|
304
|
+
name: "Fetch catalog entities",
|
|
305
|
+
input: {
|
|
306
|
+
entityRefs: ["component:default/name"]
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
]
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const id$2 = "catalog:fetch";
|
|
315
|
+
function createFetchCatalogEntityAction(options) {
|
|
316
|
+
const { catalogClient } = options;
|
|
317
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
318
|
+
id: id$2,
|
|
319
|
+
description: "Returns entity or entities from the catalog by entity reference(s)",
|
|
320
|
+
examples: examples$7,
|
|
321
|
+
supportsDryRun: true,
|
|
322
|
+
schema: {
|
|
323
|
+
input: zod.z.object({
|
|
324
|
+
entityRef: zod.z.string({
|
|
325
|
+
description: "Entity reference of the entity to get"
|
|
326
|
+
}).optional(),
|
|
327
|
+
entityRefs: zod.z.array(zod.z.string(), {
|
|
328
|
+
description: "Entity references of the entities to get"
|
|
329
|
+
}).optional(),
|
|
330
|
+
optional: zod.z.boolean({
|
|
331
|
+
description: "Allow the entity or entities to optionally exist. Default: false"
|
|
332
|
+
}).optional(),
|
|
333
|
+
defaultKind: zod.z.string({ description: "The default kind" }).optional(),
|
|
334
|
+
defaultNamespace: zod.z.string({ description: "The default namespace" }).optional()
|
|
335
|
+
}),
|
|
336
|
+
output: zod.z.object({
|
|
337
|
+
entity: zod.z.any({
|
|
338
|
+
description: "Object containing same values used in the Entity schema. Only when used with `entityRef` parameter."
|
|
339
|
+
}).optional(),
|
|
340
|
+
entities: zod.z.array(
|
|
341
|
+
zod.z.any({
|
|
342
|
+
description: "Array containing objects with same values used in the Entity schema. Only when used with `entityRefs` parameter."
|
|
343
|
+
})
|
|
344
|
+
).optional()
|
|
345
|
+
})
|
|
346
|
+
},
|
|
347
|
+
async handler(ctx) {
|
|
348
|
+
var _a, _b;
|
|
349
|
+
const { entityRef, entityRefs, optional, defaultKind, defaultNamespace } = ctx.input;
|
|
350
|
+
if (!entityRef && !entityRefs) {
|
|
351
|
+
if (optional) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
throw new Error("Missing entity reference or references");
|
|
355
|
+
}
|
|
356
|
+
if (entityRef) {
|
|
357
|
+
const entity = await catalogClient.getEntityByRef(
|
|
358
|
+
catalogModel.stringifyEntityRef(
|
|
359
|
+
catalogModel.parseEntityRef(entityRef, { defaultKind, defaultNamespace })
|
|
360
|
+
),
|
|
361
|
+
{
|
|
362
|
+
token: (_a = ctx.secrets) == null ? void 0 : _a.backstageToken
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
if (!entity && !optional) {
|
|
366
|
+
throw new Error(`Entity ${entityRef} not found`);
|
|
367
|
+
}
|
|
368
|
+
ctx.output("entity", entity != null ? entity : null);
|
|
369
|
+
}
|
|
370
|
+
if (entityRefs) {
|
|
371
|
+
const entities = await catalogClient.getEntitiesByRefs(
|
|
372
|
+
{
|
|
373
|
+
entityRefs: entityRefs.map(
|
|
374
|
+
(ref) => catalogModel.stringifyEntityRef(
|
|
375
|
+
catalogModel.parseEntityRef(ref, { defaultKind, defaultNamespace })
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
token: (_b = ctx.secrets) == null ? void 0 : _b.backstageToken
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
const finalEntities = entities.items.map((e, i) => {
|
|
384
|
+
if (!e && !optional) {
|
|
385
|
+
throw new Error(`Entity ${entityRefs[i]} not found`);
|
|
386
|
+
}
|
|
387
|
+
return e != null ? e : null;
|
|
388
|
+
});
|
|
389
|
+
ctx.output("entities", finalEntities);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const examples$6 = [
|
|
396
|
+
{
|
|
397
|
+
description: "Write a debug message",
|
|
398
|
+
example: yaml__default["default"].stringify({
|
|
399
|
+
steps: [
|
|
400
|
+
{
|
|
401
|
+
action: "debug:log",
|
|
402
|
+
id: "write-debug-line",
|
|
403
|
+
name: 'Write "Hello Backstage!" log line',
|
|
404
|
+
input: {
|
|
405
|
+
message: "Hello Backstage!"
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
]
|
|
409
|
+
})
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
description: "List the workspace directory",
|
|
413
|
+
example: yaml__default["default"].stringify({
|
|
414
|
+
steps: [
|
|
415
|
+
{
|
|
416
|
+
action: "debug:log",
|
|
417
|
+
id: "write-workspace-directory",
|
|
418
|
+
name: "List the workspace directory",
|
|
419
|
+
input: {
|
|
420
|
+
listWorkspace: true
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
]
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
const id$1 = "debug:log";
|
|
429
|
+
function createDebugLogAction() {
|
|
430
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
431
|
+
id: id$1,
|
|
432
|
+
description: "Writes a message into the log or lists all files in the workspace.",
|
|
433
|
+
examples: examples$6,
|
|
434
|
+
schema: {
|
|
435
|
+
input: {
|
|
436
|
+
type: "object",
|
|
437
|
+
properties: {
|
|
438
|
+
message: {
|
|
439
|
+
title: "Message to output.",
|
|
440
|
+
type: "string"
|
|
441
|
+
},
|
|
442
|
+
listWorkspace: {
|
|
443
|
+
title: "List all files in the workspace, if true.",
|
|
444
|
+
type: "boolean"
|
|
445
|
+
},
|
|
446
|
+
extra: {
|
|
447
|
+
title: "Extra info"
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
supportsDryRun: true,
|
|
453
|
+
async handler(ctx) {
|
|
454
|
+
var _a, _b;
|
|
455
|
+
ctx.logger.info(JSON.stringify(ctx.input, null, 2));
|
|
456
|
+
if ((_a = ctx.input) == null ? void 0 : _a.message) {
|
|
457
|
+
ctx.logStream.write(ctx.input.message);
|
|
458
|
+
}
|
|
459
|
+
if ((_b = ctx.input) == null ? void 0 : _b.listWorkspace) {
|
|
460
|
+
const files = await recursiveReadDir(ctx.workspacePath);
|
|
461
|
+
ctx.logStream.write(
|
|
462
|
+
`Workspace:
|
|
463
|
+
${files.map((f) => ` - ${path.relative(ctx.workspacePath, f)}`).join("\n")}`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
async function recursiveReadDir(dir) {
|
|
470
|
+
const subdirs = await fs.readdir(dir);
|
|
471
|
+
const files = await Promise.all(
|
|
472
|
+
subdirs.map(async (subdir) => {
|
|
473
|
+
const res = path.join(dir, subdir);
|
|
474
|
+
return (await fs.stat(res)).isDirectory() ? recursiveReadDir(res) : [res];
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
return files.reduce((a, f) => a.concat(f), []);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const examples$5 = [
|
|
481
|
+
{
|
|
482
|
+
description: "Waiting for 50 milliseconds",
|
|
483
|
+
example: yaml__default["default"].stringify({
|
|
484
|
+
steps: [
|
|
485
|
+
{
|
|
486
|
+
action: "debug:wait",
|
|
487
|
+
id: "wait-milliseconds",
|
|
488
|
+
name: "Waiting for 50 milliseconds",
|
|
489
|
+
input: {
|
|
490
|
+
milliseconds: 50
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
})
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
description: "Waiting for 5 seconds",
|
|
498
|
+
example: yaml__default["default"].stringify({
|
|
499
|
+
steps: [
|
|
500
|
+
{
|
|
501
|
+
action: "debug:wait",
|
|
502
|
+
id: "wait-5sec",
|
|
503
|
+
name: "Waiting for 5 seconds",
|
|
504
|
+
input: {
|
|
505
|
+
seconds: 5
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
})
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
description: "Waiting for 1 minutes",
|
|
513
|
+
example: yaml__default["default"].stringify({
|
|
514
|
+
steps: [
|
|
515
|
+
{
|
|
516
|
+
action: "debug:wait",
|
|
517
|
+
id: "wait-1min",
|
|
518
|
+
name: "Waiting for 1 minutes",
|
|
519
|
+
input: {
|
|
520
|
+
minutes: 1
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
];
|
|
527
|
+
|
|
528
|
+
const id = "debug:wait";
|
|
529
|
+
const MAX_WAIT_TIME_IN_ISO = "T00:00:30";
|
|
530
|
+
function createWaitAction(options) {
|
|
531
|
+
const toDuration = (maxWaitTime) => {
|
|
532
|
+
if (maxWaitTime) {
|
|
533
|
+
if (maxWaitTime instanceof luxon.Duration) {
|
|
534
|
+
return maxWaitTime;
|
|
535
|
+
}
|
|
536
|
+
return luxon.Duration.fromObject(maxWaitTime);
|
|
537
|
+
}
|
|
538
|
+
return luxon.Duration.fromISOTime(MAX_WAIT_TIME_IN_ISO);
|
|
539
|
+
};
|
|
540
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
541
|
+
id,
|
|
542
|
+
description: "Waits for a certain period of time.",
|
|
543
|
+
examples: examples$5,
|
|
544
|
+
schema: {
|
|
545
|
+
input: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: {
|
|
548
|
+
minutes: {
|
|
549
|
+
title: "Waiting period in minutes.",
|
|
550
|
+
type: "number"
|
|
551
|
+
},
|
|
552
|
+
seconds: {
|
|
553
|
+
title: "Waiting period in seconds.",
|
|
554
|
+
type: "number"
|
|
555
|
+
},
|
|
556
|
+
milliseconds: {
|
|
557
|
+
title: "Waiting period in milliseconds.",
|
|
558
|
+
type: "number"
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
async handler(ctx) {
|
|
564
|
+
const delayTime = luxon.Duration.fromObject(ctx.input);
|
|
565
|
+
const maxWait = toDuration(options == null ? void 0 : options.maxWaitTime);
|
|
566
|
+
if (delayTime.minus(maxWait).toMillis() > 0) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`Waiting duration is longer than the maximum threshold of ${maxWait.toHuman()}`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
await new Promise((resolve) => {
|
|
572
|
+
var _a;
|
|
573
|
+
const controller = new AbortController();
|
|
574
|
+
const timeoutHandle = setTimeout(abort, delayTime.toMillis());
|
|
575
|
+
(_a = ctx.signal) == null ? void 0 : _a.addEventListener("abort", abort);
|
|
576
|
+
function abort() {
|
|
577
|
+
var _a2;
|
|
578
|
+
(_a2 = ctx.signal) == null ? void 0 : _a2.removeEventListener("abort", abort);
|
|
579
|
+
clearTimeout(timeoutHandle);
|
|
580
|
+
controller.abort();
|
|
581
|
+
resolve("finished");
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const examples$4 = [
|
|
589
|
+
{
|
|
590
|
+
description: "Downloads content and places it in the workspace.",
|
|
591
|
+
example: yaml__default["default"].stringify({
|
|
592
|
+
steps: [
|
|
593
|
+
{
|
|
594
|
+
action: "fetch:plain",
|
|
595
|
+
id: "fetch-plain",
|
|
596
|
+
name: "Fetch plain",
|
|
597
|
+
input: {
|
|
598
|
+
url: "https://github.com/backstage/community/tree/main/backstage-community-sessions/assets"
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
]
|
|
602
|
+
})
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
description: "Optionally, if you would prefer the data to be downloaded to a subdirectory in the workspace you may specify the \u2018targetPath\u2019 input option.",
|
|
606
|
+
example: yaml__default["default"].stringify({
|
|
607
|
+
steps: [
|
|
608
|
+
{
|
|
609
|
+
action: "fetch:plain",
|
|
610
|
+
id: "fetch-plain",
|
|
611
|
+
name: "Fetch plain",
|
|
612
|
+
input: {
|
|
613
|
+
url: "https://github.com/backstage/community/tree/main/backstage-community-sessions/assets",
|
|
614
|
+
targetPath: "fetched-data"
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
]
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
];
|
|
621
|
+
|
|
622
|
+
const ACTION_ID = "fetch:plain";
|
|
623
|
+
function createFetchPlainAction(options) {
|
|
624
|
+
const { reader, integrations } = options;
|
|
625
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
626
|
+
id: ACTION_ID,
|
|
627
|
+
examples: examples$4,
|
|
628
|
+
description: "Downloads content and places it in the workspace, or optionally in a subdirectory specified by the `targetPath` input option.",
|
|
629
|
+
schema: {
|
|
630
|
+
input: {
|
|
631
|
+
type: "object",
|
|
632
|
+
required: ["url"],
|
|
633
|
+
properties: {
|
|
634
|
+
url: {
|
|
635
|
+
title: "Fetch URL",
|
|
636
|
+
description: "Relative path or absolute URL pointing to the directory tree to fetch",
|
|
637
|
+
type: "string"
|
|
638
|
+
},
|
|
639
|
+
targetPath: {
|
|
640
|
+
title: "Target Path",
|
|
641
|
+
description: "Target path within the working directory to download the contents to.",
|
|
642
|
+
type: "string"
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
supportsDryRun: true,
|
|
648
|
+
async handler(ctx) {
|
|
649
|
+
var _a, _b;
|
|
650
|
+
ctx.logger.info("Fetching plain content from remote URL");
|
|
651
|
+
const targetPath = (_a = ctx.input.targetPath) != null ? _a : "./";
|
|
652
|
+
const outputPath = backendCommon.resolveSafeChildPath(ctx.workspacePath, targetPath);
|
|
653
|
+
await pluginScaffolderNode.fetchContents({
|
|
654
|
+
reader,
|
|
655
|
+
integrations,
|
|
656
|
+
baseUrl: (_b = ctx.templateInfo) == null ? void 0 : _b.baseUrl,
|
|
657
|
+
fetchUrl: ctx.input.url,
|
|
658
|
+
outputPath
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const examples$3 = [
|
|
665
|
+
{
|
|
666
|
+
description: "Downloads a file and places it in the workspace.",
|
|
667
|
+
example: yaml__default["default"].stringify({
|
|
668
|
+
steps: [
|
|
669
|
+
{
|
|
670
|
+
action: "fetch:plain:file",
|
|
671
|
+
id: "fetch-plain-file",
|
|
672
|
+
name: "Fetch plain file",
|
|
673
|
+
input: {
|
|
674
|
+
url: "https://github.com/backstage/community/tree/main/backstage-community-sessions/assets/Backstage%20Community%20Sessions.png",
|
|
675
|
+
targetPath: "target-path"
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
]
|
|
679
|
+
})
|
|
680
|
+
}
|
|
681
|
+
];
|
|
682
|
+
|
|
683
|
+
function createFetchPlainFileAction(options) {
|
|
684
|
+
const { reader, integrations } = options;
|
|
685
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
686
|
+
id: "fetch:plain:file",
|
|
687
|
+
description: "Downloads single file and places it in the workspace.",
|
|
688
|
+
examples: examples$3,
|
|
689
|
+
schema: {
|
|
690
|
+
input: {
|
|
691
|
+
type: "object",
|
|
692
|
+
required: ["url", "targetPath"],
|
|
693
|
+
properties: {
|
|
694
|
+
url: {
|
|
695
|
+
title: "Fetch URL",
|
|
696
|
+
description: "Relative path or absolute URL pointing to the single file to fetch.",
|
|
697
|
+
type: "string"
|
|
698
|
+
},
|
|
699
|
+
targetPath: {
|
|
700
|
+
title: "Target Path",
|
|
701
|
+
description: "Target path within the working directory to download the file as.",
|
|
702
|
+
type: "string"
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
supportsDryRun: true,
|
|
708
|
+
async handler(ctx) {
|
|
709
|
+
var _a;
|
|
710
|
+
ctx.logger.info("Fetching plain content from remote URL");
|
|
711
|
+
const outputPath = backendCommon.resolveSafeChildPath(
|
|
712
|
+
ctx.workspacePath,
|
|
713
|
+
ctx.input.targetPath
|
|
714
|
+
);
|
|
715
|
+
await pluginScaffolderNode.fetchFile({
|
|
716
|
+
reader,
|
|
717
|
+
integrations,
|
|
718
|
+
baseUrl: (_a = ctx.templateInfo) == null ? void 0 : _a.baseUrl,
|
|
719
|
+
fetchUrl: ctx.input.url,
|
|
720
|
+
outputPath
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const mkScript = (nunjucksSource) => `
|
|
727
|
+
const { render, renderCompat } = (() => {
|
|
728
|
+
const module = {};
|
|
729
|
+
const process = { env: {} };
|
|
730
|
+
const require = (pkg) => { if (pkg === 'events') { return function (){}; }};
|
|
731
|
+
|
|
732
|
+
${nunjucksSource}
|
|
733
|
+
|
|
734
|
+
const env = module.exports.configure({
|
|
735
|
+
autoescape: false,
|
|
736
|
+
tags: {
|
|
737
|
+
variableStart: '\${{',
|
|
738
|
+
variableEnd: '}}',
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const compatEnv = module.exports.configure({
|
|
743
|
+
autoescape: false,
|
|
744
|
+
tags: {
|
|
745
|
+
variableStart: '{{',
|
|
746
|
+
variableEnd: '}}',
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
compatEnv.addFilter('jsonify', compatEnv.getFilter('dump'));
|
|
750
|
+
|
|
751
|
+
for (const name of JSON.parse(availableTemplateFilters)) {
|
|
752
|
+
env.addFilter(name, (...args) => JSON.parse(callFilter(name, args)));
|
|
753
|
+
}
|
|
754
|
+
for (const [name, value] of Object.entries(JSON.parse(availableTemplateGlobals))) {
|
|
755
|
+
env.addGlobal(name, value);
|
|
756
|
+
}
|
|
757
|
+
for (const name of JSON.parse(availableTemplateCallbacks)) {
|
|
758
|
+
env.addGlobal(name, (...args) => JSON.parse(callGlobal(name, args)));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
let uninstallCompat = undefined;
|
|
762
|
+
|
|
763
|
+
function render(str, values) {
|
|
764
|
+
try {
|
|
765
|
+
if (uninstallCompat) {
|
|
766
|
+
uninstallCompat();
|
|
767
|
+
uninstallCompat = undefined;
|
|
768
|
+
}
|
|
769
|
+
return env.renderString(str, JSON.parse(values));
|
|
770
|
+
} catch (error) {
|
|
771
|
+
// Make sure errors don't leak anything
|
|
772
|
+
throw new Error(String(error.message));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function renderCompat(str, values) {
|
|
777
|
+
try {
|
|
778
|
+
if (!uninstallCompat) {
|
|
779
|
+
uninstallCompat = module.exports.installJinjaCompat();
|
|
780
|
+
}
|
|
781
|
+
return compatEnv.renderString(str, JSON.parse(values));
|
|
782
|
+
} catch (error) {
|
|
783
|
+
// Make sure errors don't leak anything
|
|
784
|
+
throw new Error(String(error.message));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return { render, renderCompat };
|
|
789
|
+
})();
|
|
790
|
+
`;
|
|
791
|
+
class SecureTemplater {
|
|
792
|
+
static async loadRenderer(options = {}) {
|
|
793
|
+
const {
|
|
794
|
+
cookiecutterCompat,
|
|
795
|
+
templateFilters = {},
|
|
796
|
+
templateGlobals = {}
|
|
797
|
+
} = options;
|
|
798
|
+
const isolate = new isolatedVm.Isolate({ memoryLimit: 128 });
|
|
799
|
+
const context = await isolate.createContext();
|
|
800
|
+
const contextGlobal = context.global;
|
|
801
|
+
const nunjucksSource = await fs__default["default"].readFile(
|
|
802
|
+
backendCommon.resolvePackagePath(
|
|
803
|
+
"@backstage/plugin-scaffolder-backend",
|
|
804
|
+
"assets/nunjucks.js.txt"
|
|
805
|
+
),
|
|
806
|
+
"utf-8"
|
|
807
|
+
);
|
|
808
|
+
const nunjucksScript = await isolate.compileScript(
|
|
809
|
+
mkScript(nunjucksSource)
|
|
810
|
+
);
|
|
811
|
+
const availableFilters = Object.keys(templateFilters);
|
|
812
|
+
await contextGlobal.set(
|
|
813
|
+
"availableTemplateFilters",
|
|
814
|
+
JSON.stringify(availableFilters)
|
|
815
|
+
);
|
|
816
|
+
const globalCallbacks = [];
|
|
817
|
+
const globalValues = {};
|
|
818
|
+
for (const [name, value] of Object.entries(templateGlobals)) {
|
|
819
|
+
if (typeof value === "function") {
|
|
820
|
+
globalCallbacks.push(name);
|
|
821
|
+
} else {
|
|
822
|
+
globalValues[name] = value;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
await contextGlobal.set(
|
|
826
|
+
"availableTemplateGlobals",
|
|
827
|
+
JSON.stringify(globalValues)
|
|
828
|
+
);
|
|
829
|
+
await contextGlobal.set(
|
|
830
|
+
"availableTemplateCallbacks",
|
|
831
|
+
JSON.stringify(globalCallbacks)
|
|
832
|
+
);
|
|
833
|
+
await contextGlobal.set(
|
|
834
|
+
"callFilter",
|
|
835
|
+
(filterName, args) => {
|
|
836
|
+
if (!Object.hasOwn(templateFilters, filterName)) {
|
|
837
|
+
return "";
|
|
838
|
+
}
|
|
839
|
+
return JSON.stringify(templateFilters[filterName](...args));
|
|
840
|
+
}
|
|
841
|
+
);
|
|
842
|
+
await contextGlobal.set(
|
|
843
|
+
"callGlobal",
|
|
844
|
+
(globalName, args) => {
|
|
845
|
+
if (!Object.hasOwn(templateGlobals, globalName)) {
|
|
846
|
+
return "";
|
|
847
|
+
}
|
|
848
|
+
const global = templateGlobals[globalName];
|
|
849
|
+
if (typeof global !== "function") {
|
|
850
|
+
return "";
|
|
851
|
+
}
|
|
852
|
+
return JSON.stringify(global(...args));
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
await nunjucksScript.run(context);
|
|
856
|
+
const render = (template, values) => {
|
|
857
|
+
if (!context) {
|
|
858
|
+
throw new Error("SecureTemplater has not been initialized");
|
|
859
|
+
}
|
|
860
|
+
contextGlobal.setSync("templateStr", String(template));
|
|
861
|
+
contextGlobal.setSync("templateValues", JSON.stringify(values));
|
|
862
|
+
if (cookiecutterCompat) {
|
|
863
|
+
return context.evalSync(`renderCompat(templateStr, templateValues)`);
|
|
864
|
+
}
|
|
865
|
+
return context.evalSync(`render(templateStr, templateValues)`);
|
|
866
|
+
};
|
|
867
|
+
return render;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const createDefaultFilters = ({
|
|
872
|
+
integrations
|
|
873
|
+
}) => {
|
|
874
|
+
return {
|
|
875
|
+
parseRepoUrl: (url) => pluginScaffolderNode.parseRepoUrl(url, integrations),
|
|
876
|
+
parseEntityRef: (ref, context) => catalogModel.parseEntityRef(ref, context),
|
|
877
|
+
pick: (obj, key) => get__default["default"](obj, key),
|
|
878
|
+
projectSlug: (repoUrl) => {
|
|
879
|
+
const { owner, repo } = pluginScaffolderNode.parseRepoUrl(repoUrl, integrations);
|
|
880
|
+
return `${owner}/${repo}`;
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
const examples$2 = [
|
|
886
|
+
{
|
|
887
|
+
description: "Downloads a skelaton directory that lives alongside the template file and fill it out with values.",
|
|
888
|
+
example: yaml__default["default"].stringify({
|
|
889
|
+
steps: [
|
|
890
|
+
{
|
|
891
|
+
action: "fetch:template",
|
|
892
|
+
id: "fetch-template",
|
|
893
|
+
name: "Fetch template",
|
|
894
|
+
input: {
|
|
895
|
+
url: "./skeleton",
|
|
896
|
+
targetPath: "./target",
|
|
897
|
+
values: {
|
|
898
|
+
name: "test-project",
|
|
899
|
+
count: 1234,
|
|
900
|
+
itemList: ["first", "second", "third"],
|
|
901
|
+
showDummyFile: false
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
]
|
|
906
|
+
})
|
|
907
|
+
}
|
|
908
|
+
];
|
|
909
|
+
|
|
910
|
+
function createFetchTemplateAction(options) {
|
|
911
|
+
const {
|
|
912
|
+
reader,
|
|
913
|
+
integrations,
|
|
914
|
+
additionalTemplateFilters,
|
|
915
|
+
additionalTemplateGlobals
|
|
916
|
+
} = options;
|
|
917
|
+
const defaultTemplateFilters = createDefaultFilters({ integrations });
|
|
918
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
919
|
+
id: "fetch:template",
|
|
920
|
+
description: "Downloads a skeleton, templates variables into file and directory names and content, and places the result in the workspace, or optionally in a subdirectory specified by the `targetPath` input option.",
|
|
921
|
+
examples: examples$2,
|
|
922
|
+
schema: {
|
|
923
|
+
input: {
|
|
924
|
+
type: "object",
|
|
925
|
+
required: ["url"],
|
|
926
|
+
properties: {
|
|
927
|
+
url: {
|
|
928
|
+
title: "Fetch URL",
|
|
929
|
+
description: "Relative path or absolute URL pointing to the directory tree to fetch",
|
|
930
|
+
type: "string"
|
|
931
|
+
},
|
|
932
|
+
targetPath: {
|
|
933
|
+
title: "Target Path",
|
|
934
|
+
description: "Target path within the working directory to download the contents to. Defaults to the working directory root.",
|
|
935
|
+
type: "string"
|
|
936
|
+
},
|
|
937
|
+
values: {
|
|
938
|
+
title: "Template Values",
|
|
939
|
+
description: "Values to pass on to the templating engine",
|
|
940
|
+
type: "object"
|
|
941
|
+
},
|
|
942
|
+
copyWithoutRender: {
|
|
943
|
+
title: "[Deprecated] Copy Without Render",
|
|
944
|
+
description: "An array of glob patterns. Any files or directories which match are copied without being processed as templates.",
|
|
945
|
+
type: "array",
|
|
946
|
+
items: {
|
|
947
|
+
type: "string"
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
copyWithoutTemplating: {
|
|
951
|
+
title: "Copy Without Templating",
|
|
952
|
+
description: "An array of glob patterns. Contents of matched files or directories are copied without being processed, but paths are subject to rendering.",
|
|
953
|
+
type: "array",
|
|
954
|
+
items: {
|
|
955
|
+
type: "string"
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
cookiecutterCompat: {
|
|
959
|
+
title: "Cookiecutter compatibility mode",
|
|
960
|
+
description: "Enable features to maximise compatibility with templates built for fetch:cookiecutter",
|
|
961
|
+
type: "boolean"
|
|
962
|
+
},
|
|
963
|
+
templateFileExtension: {
|
|
964
|
+
title: "Template File Extension",
|
|
965
|
+
description: "If set, only files with the given extension will be templated. If set to `true`, the default extension `.njk` is used.",
|
|
966
|
+
type: ["string", "boolean"]
|
|
967
|
+
},
|
|
968
|
+
replace: {
|
|
969
|
+
title: "Replace files",
|
|
970
|
+
description: "If set, replace files in targetPath instead of skipping existing ones.",
|
|
971
|
+
type: "boolean"
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
supportsDryRun: true,
|
|
977
|
+
async handler(ctx) {
|
|
978
|
+
var _a, _b;
|
|
979
|
+
ctx.logger.info("Fetching template content from remote URL");
|
|
980
|
+
const workDir = await ctx.createTemporaryDirectory();
|
|
981
|
+
const templateDir = backendCommon.resolveSafeChildPath(workDir, "template");
|
|
982
|
+
const targetPath = (_a = ctx.input.targetPath) != null ? _a : "./";
|
|
983
|
+
const outputDir = backendCommon.resolveSafeChildPath(ctx.workspacePath, targetPath);
|
|
984
|
+
if (ctx.input.copyWithoutRender && ctx.input.copyWithoutTemplating) {
|
|
985
|
+
throw new errors.InputError(
|
|
986
|
+
"Fetch action input copyWithoutRender and copyWithoutTemplating can not be used at the same time"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
let copyOnlyPatterns;
|
|
990
|
+
let renderFilename;
|
|
991
|
+
if (ctx.input.copyWithoutRender) {
|
|
992
|
+
ctx.logger.warn(
|
|
993
|
+
"[Deprecated] copyWithoutRender is deprecated Please use copyWithoutTemplating instead."
|
|
994
|
+
);
|
|
995
|
+
copyOnlyPatterns = ctx.input.copyWithoutRender;
|
|
996
|
+
renderFilename = false;
|
|
997
|
+
} else {
|
|
998
|
+
copyOnlyPatterns = ctx.input.copyWithoutTemplating;
|
|
999
|
+
renderFilename = true;
|
|
1000
|
+
}
|
|
1001
|
+
if (copyOnlyPatterns && !Array.isArray(copyOnlyPatterns)) {
|
|
1002
|
+
throw new errors.InputError(
|
|
1003
|
+
"Fetch action input copyWithoutRender/copyWithoutTemplating must be an Array"
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
if (ctx.input.templateFileExtension && (copyOnlyPatterns || ctx.input.cookiecutterCompat)) {
|
|
1007
|
+
throw new errors.InputError(
|
|
1008
|
+
"Fetch action input extension incompatible with copyWithoutRender/copyWithoutTemplating and cookiecutterCompat"
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
let extension = false;
|
|
1012
|
+
if (ctx.input.templateFileExtension) {
|
|
1013
|
+
extension = ctx.input.templateFileExtension === true ? ".njk" : ctx.input.templateFileExtension;
|
|
1014
|
+
if (!extension.startsWith(".")) {
|
|
1015
|
+
extension = `.${extension}`;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
await pluginScaffolderNode.fetchContents({
|
|
1019
|
+
reader,
|
|
1020
|
+
integrations,
|
|
1021
|
+
baseUrl: (_b = ctx.templateInfo) == null ? void 0 : _b.baseUrl,
|
|
1022
|
+
fetchUrl: ctx.input.url,
|
|
1023
|
+
outputPath: templateDir
|
|
1024
|
+
});
|
|
1025
|
+
ctx.logger.info("Listing files and directories in template");
|
|
1026
|
+
const allEntriesInTemplate = await globby__default["default"](`**/*`, {
|
|
1027
|
+
cwd: templateDir,
|
|
1028
|
+
dot: true,
|
|
1029
|
+
onlyFiles: false,
|
|
1030
|
+
markDirectories: true,
|
|
1031
|
+
followSymbolicLinks: false
|
|
1032
|
+
});
|
|
1033
|
+
const nonTemplatedEntries = new Set(
|
|
1034
|
+
await globby__default["default"](copyOnlyPatterns || [], {
|
|
1035
|
+
cwd: templateDir,
|
|
1036
|
+
dot: true,
|
|
1037
|
+
onlyFiles: false,
|
|
1038
|
+
markDirectories: true,
|
|
1039
|
+
followSymbolicLinks: false
|
|
1040
|
+
})
|
|
1041
|
+
);
|
|
1042
|
+
const { cookiecutterCompat, values } = ctx.input;
|
|
1043
|
+
const context = {
|
|
1044
|
+
[cookiecutterCompat ? "cookiecutter" : "values"]: values
|
|
1045
|
+
};
|
|
1046
|
+
ctx.logger.info(
|
|
1047
|
+
`Processing ${allEntriesInTemplate.length} template files/directories with input values`,
|
|
1048
|
+
ctx.input.values
|
|
1049
|
+
);
|
|
1050
|
+
const renderTemplate = await SecureTemplater.loadRenderer({
|
|
1051
|
+
cookiecutterCompat: ctx.input.cookiecutterCompat,
|
|
1052
|
+
templateFilters: {
|
|
1053
|
+
...defaultTemplateFilters,
|
|
1054
|
+
...additionalTemplateFilters
|
|
1055
|
+
},
|
|
1056
|
+
templateGlobals: additionalTemplateGlobals
|
|
1057
|
+
});
|
|
1058
|
+
for (const location of allEntriesInTemplate) {
|
|
1059
|
+
let renderContents;
|
|
1060
|
+
let localOutputPath = location;
|
|
1061
|
+
if (extension) {
|
|
1062
|
+
renderContents = path.extname(localOutputPath) === extension;
|
|
1063
|
+
if (renderContents) {
|
|
1064
|
+
localOutputPath = localOutputPath.slice(0, -extension.length);
|
|
1065
|
+
}
|
|
1066
|
+
localOutputPath = renderTemplate(localOutputPath, context);
|
|
1067
|
+
} else {
|
|
1068
|
+
renderContents = !nonTemplatedEntries.has(location);
|
|
1069
|
+
if (renderFilename) {
|
|
1070
|
+
localOutputPath = renderTemplate(localOutputPath, context);
|
|
1071
|
+
} else {
|
|
1072
|
+
localOutputPath = renderContents ? renderTemplate(localOutputPath, context) : localOutputPath;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (containsSkippedContent(localOutputPath)) {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
const outputPath = backendCommon.resolveSafeChildPath(outputDir, localOutputPath);
|
|
1079
|
+
if (fs__default["default"].existsSync(outputPath) && !ctx.input.replace) {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (!renderContents && !extension) {
|
|
1083
|
+
ctx.logger.info(
|
|
1084
|
+
`Copying file/directory ${location} without processing.`
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
if (location.endsWith("/")) {
|
|
1088
|
+
ctx.logger.info(
|
|
1089
|
+
`Writing directory ${location} to template output path.`
|
|
1090
|
+
);
|
|
1091
|
+
await fs__default["default"].ensureDir(outputPath);
|
|
1092
|
+
} else {
|
|
1093
|
+
const inputFilePath = backendCommon.resolveSafeChildPath(templateDir, location);
|
|
1094
|
+
const stats = await fs__default["default"].promises.lstat(inputFilePath);
|
|
1095
|
+
if (stats.isSymbolicLink() || await isbinaryfile.isBinaryFile(inputFilePath)) {
|
|
1096
|
+
ctx.logger.info(
|
|
1097
|
+
`Copying file binary or symbolic link at ${location}, to template output path.`
|
|
1098
|
+
);
|
|
1099
|
+
await fs__default["default"].copy(inputFilePath, outputPath);
|
|
1100
|
+
} else {
|
|
1101
|
+
const statsObj = await fs__default["default"].stat(inputFilePath);
|
|
1102
|
+
ctx.logger.info(
|
|
1103
|
+
`Writing file ${location} to template output path with mode ${statsObj.mode}.`
|
|
1104
|
+
);
|
|
1105
|
+
const inputFileContents = await fs__default["default"].readFile(inputFilePath, "utf-8");
|
|
1106
|
+
await fs__default["default"].outputFile(
|
|
1107
|
+
outputPath,
|
|
1108
|
+
renderContents ? renderTemplate(inputFileContents, context) : inputFileContents,
|
|
1109
|
+
{ mode: statsObj.mode }
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
ctx.logger.info(`Template result written to ${outputDir}`);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
function containsSkippedContent(localOutputPath) {
|
|
1119
|
+
return localOutputPath === "" || localOutputPath.startsWith("/") || localOutputPath.includes("//");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const examples$1 = [
|
|
1123
|
+
{
|
|
1124
|
+
description: "Delete specified files",
|
|
1125
|
+
example: yaml__namespace.stringify({
|
|
1126
|
+
steps: [
|
|
1127
|
+
{
|
|
1128
|
+
action: "fs:delete",
|
|
1129
|
+
id: "deleteFiles",
|
|
1130
|
+
name: "Delete files",
|
|
1131
|
+
input: {
|
|
1132
|
+
files: ["file1.txt", "file2.txt"]
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
]
|
|
1136
|
+
})
|
|
1137
|
+
}
|
|
1138
|
+
];
|
|
1139
|
+
|
|
1140
|
+
const createFilesystemDeleteAction = () => {
|
|
1141
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
1142
|
+
id: "fs:delete",
|
|
1143
|
+
description: "Deletes files and directories from the workspace",
|
|
1144
|
+
examples: examples$1,
|
|
1145
|
+
schema: {
|
|
1146
|
+
input: {
|
|
1147
|
+
required: ["files"],
|
|
1148
|
+
type: "object",
|
|
1149
|
+
properties: {
|
|
1150
|
+
files: {
|
|
1151
|
+
title: "Files",
|
|
1152
|
+
description: "A list of files and directories that will be deleted",
|
|
1153
|
+
type: "array",
|
|
1154
|
+
items: {
|
|
1155
|
+
type: "string"
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
supportsDryRun: true,
|
|
1162
|
+
async handler(ctx) {
|
|
1163
|
+
var _a;
|
|
1164
|
+
if (!Array.isArray((_a = ctx.input) == null ? void 0 : _a.files)) {
|
|
1165
|
+
throw new errors.InputError("files must be an Array");
|
|
1166
|
+
}
|
|
1167
|
+
for (const file of ctx.input.files) {
|
|
1168
|
+
const filepath = backendCommon.resolveSafeChildPath(ctx.workspacePath, file);
|
|
1169
|
+
try {
|
|
1170
|
+
await fs__default["default"].remove(filepath);
|
|
1171
|
+
ctx.logger.info(`File ${filepath} deleted successfully`);
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
ctx.logger.error(`Failed to delete file ${filepath}:`, err);
|
|
1174
|
+
throw err;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
const examples = [
|
|
1182
|
+
{
|
|
1183
|
+
description: "Rename specified files ",
|
|
1184
|
+
example: yaml__namespace.stringify({
|
|
1185
|
+
steps: [
|
|
1186
|
+
{
|
|
1187
|
+
action: "fs:rename",
|
|
1188
|
+
id: "renameFiles",
|
|
1189
|
+
name: "Rename files",
|
|
1190
|
+
input: {
|
|
1191
|
+
files: [
|
|
1192
|
+
{ from: "file1.txt", to: "file1Renamed.txt" },
|
|
1193
|
+
{ from: "file2.txt", to: "file2Renamed.txt" },
|
|
1194
|
+
{ from: "file3.txt", to: "file3Renamed.txt", overwrite: true }
|
|
1195
|
+
]
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
]
|
|
1199
|
+
})
|
|
1200
|
+
}
|
|
1201
|
+
];
|
|
1202
|
+
|
|
1203
|
+
const createFilesystemRenameAction = () => {
|
|
1204
|
+
return pluginScaffolderNode.createTemplateAction({
|
|
1205
|
+
id: "fs:rename",
|
|
1206
|
+
description: "Renames files and directories within the workspace",
|
|
1207
|
+
examples,
|
|
1208
|
+
schema: {
|
|
1209
|
+
input: {
|
|
1210
|
+
required: ["files"],
|
|
1211
|
+
type: "object",
|
|
1212
|
+
properties: {
|
|
1213
|
+
files: {
|
|
1214
|
+
title: "Files",
|
|
1215
|
+
description: "A list of file and directory names that will be renamed",
|
|
1216
|
+
type: "array",
|
|
1217
|
+
items: {
|
|
1218
|
+
type: "object",
|
|
1219
|
+
required: ["from", "to"],
|
|
1220
|
+
properties: {
|
|
1221
|
+
from: {
|
|
1222
|
+
type: "string",
|
|
1223
|
+
title: "The source location of the file to be renamed"
|
|
1224
|
+
},
|
|
1225
|
+
to: {
|
|
1226
|
+
type: "string",
|
|
1227
|
+
title: "The destination of the new file"
|
|
1228
|
+
},
|
|
1229
|
+
overwrite: {
|
|
1230
|
+
type: "boolean",
|
|
1231
|
+
title: "Overwrite existing file or directory, default is false"
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
},
|
|
1239
|
+
supportsDryRun: true,
|
|
1240
|
+
async handler(ctx) {
|
|
1241
|
+
var _a, _b;
|
|
1242
|
+
if (!Array.isArray((_a = ctx.input) == null ? void 0 : _a.files)) {
|
|
1243
|
+
throw new errors.InputError("files must be an Array");
|
|
1244
|
+
}
|
|
1245
|
+
for (const file of ctx.input.files) {
|
|
1246
|
+
if (!file.from || !file.to) {
|
|
1247
|
+
throw new errors.InputError("each file must have a from and to property");
|
|
1248
|
+
}
|
|
1249
|
+
const sourceFilepath = backendCommon.resolveSafeChildPath(
|
|
1250
|
+
ctx.workspacePath,
|
|
1251
|
+
file.from
|
|
1252
|
+
);
|
|
1253
|
+
const destFilepath = backendCommon.resolveSafeChildPath(ctx.workspacePath, file.to);
|
|
1254
|
+
try {
|
|
1255
|
+
await fs__default["default"].move(sourceFilepath, destFilepath, {
|
|
1256
|
+
overwrite: (_b = file.overwrite) != null ? _b : false
|
|
1257
|
+
});
|
|
1258
|
+
ctx.logger.info(
|
|
1259
|
+
`File ${sourceFilepath} renamed to ${destFilepath} successfully`
|
|
1260
|
+
);
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
ctx.logger.error(
|
|
1263
|
+
`Failed to rename file ${sourceFilepath} to ${destFilepath}:`,
|
|
1264
|
+
err
|
|
1265
|
+
);
|
|
1266
|
+
throw err;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
const createBuiltinActions = (options) => {
|
|
1274
|
+
const {
|
|
1275
|
+
reader,
|
|
1276
|
+
integrations,
|
|
1277
|
+
catalogClient,
|
|
1278
|
+
config,
|
|
1279
|
+
additionalTemplateFilters,
|
|
1280
|
+
additionalTemplateGlobals
|
|
1281
|
+
} = options;
|
|
1282
|
+
const githubCredentialsProvider = integration.DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
|
1283
|
+
const actions = [
|
|
1284
|
+
createFetchPlainAction({
|
|
1285
|
+
reader,
|
|
1286
|
+
integrations
|
|
1287
|
+
}),
|
|
1288
|
+
createFetchPlainFileAction({
|
|
1289
|
+
reader,
|
|
1290
|
+
integrations
|
|
1291
|
+
}),
|
|
1292
|
+
createFetchTemplateAction({
|
|
1293
|
+
integrations,
|
|
1294
|
+
reader,
|
|
1295
|
+
additionalTemplateFilters,
|
|
1296
|
+
additionalTemplateGlobals
|
|
1297
|
+
}),
|
|
1298
|
+
gerrit.createPublishGerritAction({
|
|
1299
|
+
integrations,
|
|
1300
|
+
config
|
|
1301
|
+
}),
|
|
1302
|
+
gerrit.createPublishGerritReviewAction({
|
|
1303
|
+
integrations,
|
|
1304
|
+
config
|
|
1305
|
+
}),
|
|
1306
|
+
github.createPublishGithubAction({
|
|
1307
|
+
integrations,
|
|
1308
|
+
config,
|
|
1309
|
+
githubCredentialsProvider
|
|
1310
|
+
}),
|
|
1311
|
+
github.createPublishGithubPullRequestAction({
|
|
1312
|
+
integrations,
|
|
1313
|
+
githubCredentialsProvider
|
|
1314
|
+
}),
|
|
1315
|
+
gitlab.createPublishGitlabAction({
|
|
1316
|
+
integrations,
|
|
1317
|
+
config
|
|
1318
|
+
}),
|
|
1319
|
+
gitlab.createPublishGitlabMergeRequestAction({
|
|
1320
|
+
integrations
|
|
1321
|
+
}),
|
|
1322
|
+
bitbucket.createPublishBitbucketAction({
|
|
1323
|
+
integrations,
|
|
1324
|
+
config
|
|
1325
|
+
}),
|
|
1326
|
+
bitbucket.createPublishBitbucketCloudAction({
|
|
1327
|
+
integrations,
|
|
1328
|
+
config
|
|
1329
|
+
}),
|
|
1330
|
+
bitbucket.createPublishBitbucketServerAction({
|
|
1331
|
+
integrations,
|
|
1332
|
+
config
|
|
1333
|
+
}),
|
|
1334
|
+
bitbucket.createPublishBitbucketServerPullRequestAction({
|
|
1335
|
+
integrations,
|
|
1336
|
+
config
|
|
1337
|
+
}),
|
|
1338
|
+
azure.createPublishAzureAction({
|
|
1339
|
+
integrations,
|
|
1340
|
+
config
|
|
1341
|
+
}),
|
|
1342
|
+
createDebugLogAction(),
|
|
1343
|
+
createWaitAction(),
|
|
1344
|
+
createCatalogRegisterAction({ catalogClient, integrations }),
|
|
1345
|
+
createFetchCatalogEntityAction({ catalogClient }),
|
|
1346
|
+
createCatalogWriteAction(),
|
|
1347
|
+
createFilesystemDeleteAction(),
|
|
1348
|
+
createFilesystemRenameAction(),
|
|
1349
|
+
github.createGithubActionsDispatchAction({
|
|
1350
|
+
integrations,
|
|
1351
|
+
githubCredentialsProvider
|
|
1352
|
+
}),
|
|
1353
|
+
github.createGithubWebhookAction({
|
|
1354
|
+
integrations,
|
|
1355
|
+
githubCredentialsProvider
|
|
1356
|
+
}),
|
|
1357
|
+
github.createGithubIssuesLabelAction({
|
|
1358
|
+
integrations,
|
|
1359
|
+
githubCredentialsProvider
|
|
1360
|
+
}),
|
|
1361
|
+
github.createGithubRepoCreateAction({
|
|
1362
|
+
integrations,
|
|
1363
|
+
githubCredentialsProvider
|
|
1364
|
+
}),
|
|
1365
|
+
github.createGithubRepoPushAction({
|
|
1366
|
+
integrations,
|
|
1367
|
+
config,
|
|
1368
|
+
githubCredentialsProvider
|
|
1369
|
+
}),
|
|
1370
|
+
github.createGithubEnvironmentAction({
|
|
1371
|
+
integrations
|
|
1372
|
+
}),
|
|
1373
|
+
github.createGithubDeployKeyAction({
|
|
1374
|
+
integrations
|
|
1375
|
+
})
|
|
1376
|
+
];
|
|
1377
|
+
return actions;
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
var __defProp$4 = Object.defineProperty;
|
|
1381
|
+
var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1382
|
+
var __publicField$4 = (obj, key, value) => {
|
|
1383
|
+
__defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1384
|
+
return value;
|
|
1385
|
+
};
|
|
1386
|
+
class TemplateActionRegistry {
|
|
1387
|
+
constructor() {
|
|
1388
|
+
__publicField$4(this, "actions", /* @__PURE__ */ new Map());
|
|
1389
|
+
}
|
|
1390
|
+
register(action) {
|
|
1391
|
+
if (this.actions.has(action.id)) {
|
|
1392
|
+
throw new errors.ConflictError(
|
|
1393
|
+
`Template action with ID '${action.id}' has already been registered`
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
this.actions.set(action.id, action);
|
|
1397
|
+
}
|
|
1398
|
+
get(actionId) {
|
|
1399
|
+
const action = this.actions.get(actionId);
|
|
1400
|
+
if (!action) {
|
|
1401
|
+
throw new errors.NotFoundError(
|
|
1402
|
+
`Template action with ID '${actionId}' is not registered.`
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
return action;
|
|
1406
|
+
}
|
|
1407
|
+
list() {
|
|
1408
|
+
return [...this.actions.values()];
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
var __defProp$3 = Object.defineProperty;
|
|
1413
|
+
var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1414
|
+
var __publicField$3 = (obj, key, value) => {
|
|
1415
|
+
__defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1416
|
+
return value;
|
|
1417
|
+
};
|
|
1418
|
+
const migrationsDir = backendCommon.resolvePackagePath(
|
|
1419
|
+
"@backstage/plugin-scaffolder-backend",
|
|
1420
|
+
"migrations"
|
|
1421
|
+
);
|
|
1422
|
+
function isPluginDatabaseManager(opt) {
|
|
1423
|
+
return opt.getClient !== void 0;
|
|
1424
|
+
}
|
|
1425
|
+
const parseSqlDateToIsoString = (input) => {
|
|
1426
|
+
if (typeof input === "string") {
|
|
1427
|
+
const parsed = luxon.DateTime.fromSQL(input, { zone: "UTC" });
|
|
1428
|
+
if (!parsed.isValid) {
|
|
1429
|
+
throw new Error(
|
|
1430
|
+
`Failed to parse database timestamp '${input}', ${parsed.invalidReason}: ${parsed.invalidExplanation}`
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
return parsed.toISO();
|
|
1434
|
+
}
|
|
1435
|
+
return input;
|
|
1436
|
+
};
|
|
1437
|
+
class DatabaseTaskStore {
|
|
1438
|
+
constructor(client) {
|
|
1439
|
+
__publicField$3(this, "db");
|
|
1440
|
+
this.db = client;
|
|
1441
|
+
}
|
|
1442
|
+
static async create(options) {
|
|
1443
|
+
const { database } = options;
|
|
1444
|
+
const client = await this.getClient(database);
|
|
1445
|
+
await this.runMigrations(database, client);
|
|
1446
|
+
return new DatabaseTaskStore(client);
|
|
1447
|
+
}
|
|
1448
|
+
static async getClient(database) {
|
|
1449
|
+
if (isPluginDatabaseManager(database)) {
|
|
1450
|
+
return database.getClient();
|
|
1451
|
+
}
|
|
1452
|
+
return database;
|
|
1453
|
+
}
|
|
1454
|
+
static async runMigrations(database, client) {
|
|
1455
|
+
var _a;
|
|
1456
|
+
if (!isPluginDatabaseManager(database)) {
|
|
1457
|
+
await client.migrate.latest({
|
|
1458
|
+
directory: migrationsDir
|
|
1459
|
+
});
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
if (!((_a = database.migrations) == null ? void 0 : _a.skip)) {
|
|
1463
|
+
await client.migrate.latest({
|
|
1464
|
+
directory: migrationsDir
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
async list(options) {
|
|
1469
|
+
const queryBuilder = this.db("tasks");
|
|
1470
|
+
if (options.createdBy) {
|
|
1471
|
+
queryBuilder.where({
|
|
1472
|
+
created_by: options.createdBy
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
const results = await queryBuilder.orderBy("created_at", "desc").select();
|
|
1476
|
+
const tasks = results.map((result) => {
|
|
1477
|
+
var _a;
|
|
1478
|
+
return {
|
|
1479
|
+
id: result.id,
|
|
1480
|
+
spec: JSON.parse(result.spec),
|
|
1481
|
+
status: result.status,
|
|
1482
|
+
createdBy: (_a = result.created_by) != null ? _a : void 0,
|
|
1483
|
+
lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at),
|
|
1484
|
+
createdAt: parseSqlDateToIsoString(result.created_at)
|
|
1485
|
+
};
|
|
1486
|
+
});
|
|
1487
|
+
return { tasks };
|
|
1488
|
+
}
|
|
1489
|
+
async getTask(taskId) {
|
|
1490
|
+
var _a;
|
|
1491
|
+
const [result] = await this.db("tasks").where({ id: taskId }).select();
|
|
1492
|
+
if (!result) {
|
|
1493
|
+
throw new errors.NotFoundError(`No task with id '${taskId}' found`);
|
|
1494
|
+
}
|
|
1495
|
+
try {
|
|
1496
|
+
const spec = JSON.parse(result.spec);
|
|
1497
|
+
const secrets = result.secrets ? JSON.parse(result.secrets) : void 0;
|
|
1498
|
+
return {
|
|
1499
|
+
id: result.id,
|
|
1500
|
+
spec,
|
|
1501
|
+
status: result.status,
|
|
1502
|
+
lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at),
|
|
1503
|
+
createdAt: parseSqlDateToIsoString(result.created_at),
|
|
1504
|
+
createdBy: (_a = result.created_by) != null ? _a : void 0,
|
|
1505
|
+
secrets
|
|
1506
|
+
};
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
throw new Error(`Failed to parse spec of task '${taskId}', ${error}`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
async createTask(options) {
|
|
1512
|
+
var _a;
|
|
1513
|
+
const taskId = uuid.v4();
|
|
1514
|
+
await this.db("tasks").insert({
|
|
1515
|
+
id: taskId,
|
|
1516
|
+
spec: JSON.stringify(options.spec),
|
|
1517
|
+
secrets: options.secrets ? JSON.stringify(options.secrets) : void 0,
|
|
1518
|
+
created_by: (_a = options.createdBy) != null ? _a : null,
|
|
1519
|
+
status: "open"
|
|
1520
|
+
});
|
|
1521
|
+
return { taskId };
|
|
1522
|
+
}
|
|
1523
|
+
async claimTask() {
|
|
1524
|
+
return this.db.transaction(async (tx) => {
|
|
1525
|
+
var _a;
|
|
1526
|
+
const [task] = await tx("tasks").where({
|
|
1527
|
+
status: "open"
|
|
1528
|
+
}).limit(1).select();
|
|
1529
|
+
if (!task) {
|
|
1530
|
+
return void 0;
|
|
1531
|
+
}
|
|
1532
|
+
const updateCount = await tx("tasks").where({ id: task.id, status: "open" }).update({
|
|
1533
|
+
status: "processing",
|
|
1534
|
+
last_heartbeat_at: this.db.fn.now(),
|
|
1535
|
+
// remove the secrets when moving to processing state.
|
|
1536
|
+
secrets: null
|
|
1537
|
+
});
|
|
1538
|
+
if (updateCount < 1) {
|
|
1539
|
+
return void 0;
|
|
1540
|
+
}
|
|
1541
|
+
try {
|
|
1542
|
+
const spec = JSON.parse(task.spec);
|
|
1543
|
+
const secrets = task.secrets ? JSON.parse(task.secrets) : void 0;
|
|
1544
|
+
return {
|
|
1545
|
+
id: task.id,
|
|
1546
|
+
spec,
|
|
1547
|
+
status: "processing",
|
|
1548
|
+
lastHeartbeatAt: task.last_heartbeat_at,
|
|
1549
|
+
createdAt: task.created_at,
|
|
1550
|
+
createdBy: (_a = task.created_by) != null ? _a : void 0,
|
|
1551
|
+
secrets
|
|
1552
|
+
};
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
throw new Error(`Failed to parse spec of task '${task.id}', ${error}`);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
async heartbeatTask(taskId) {
|
|
1559
|
+
const updateCount = await this.db("tasks").where({ id: taskId, status: "processing" }).update({
|
|
1560
|
+
last_heartbeat_at: this.db.fn.now()
|
|
1561
|
+
});
|
|
1562
|
+
if (updateCount === 0) {
|
|
1563
|
+
throw new errors.ConflictError(`No running task with taskId ${taskId} found`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
async listStaleTasks(options) {
|
|
1567
|
+
const { timeoutS } = options;
|
|
1568
|
+
let heartbeatInterval = this.db.raw(`? - interval '${timeoutS} seconds'`, [
|
|
1569
|
+
this.db.fn.now()
|
|
1570
|
+
]);
|
|
1571
|
+
if (this.db.client.config.client.includes("mysql")) {
|
|
1572
|
+
heartbeatInterval = this.db.raw(
|
|
1573
|
+
`date_sub(now(), interval ${timeoutS} second)`
|
|
1574
|
+
);
|
|
1575
|
+
} else if (this.db.client.config.client.includes("sqlite3")) {
|
|
1576
|
+
heartbeatInterval = this.db.raw(`datetime('now', ?)`, [
|
|
1577
|
+
`-${timeoutS} seconds`
|
|
1578
|
+
]);
|
|
1579
|
+
}
|
|
1580
|
+
const rawRows = await this.db("tasks").where("status", "processing").andWhere("last_heartbeat_at", "<=", heartbeatInterval);
|
|
1581
|
+
const tasks = rawRows.map((row) => ({
|
|
1582
|
+
taskId: row.id
|
|
1583
|
+
}));
|
|
1584
|
+
return { tasks };
|
|
1585
|
+
}
|
|
1586
|
+
async completeTask(options) {
|
|
1587
|
+
const { taskId, status, eventBody } = options;
|
|
1588
|
+
let oldStatus;
|
|
1589
|
+
if (["failed", "completed", "cancelled"].includes(status)) {
|
|
1590
|
+
oldStatus = "processing";
|
|
1591
|
+
} else {
|
|
1592
|
+
throw new Error(
|
|
1593
|
+
`Invalid status update of run '${taskId}' to status '${status}'`
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
await this.db.transaction(async (tx) => {
|
|
1597
|
+
const [task] = await tx("tasks").where({
|
|
1598
|
+
id: taskId
|
|
1599
|
+
}).limit(1).select();
|
|
1600
|
+
const updateTask = async (criteria) => {
|
|
1601
|
+
const updateCount = await tx("tasks").where(criteria).update({
|
|
1602
|
+
status
|
|
1603
|
+
});
|
|
1604
|
+
if (updateCount !== 1) {
|
|
1605
|
+
throw new errors.ConflictError(
|
|
1606
|
+
`Failed to update status to '${status}' for taskId ${taskId}`
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
await tx("task_events").insert({
|
|
1610
|
+
task_id: taskId,
|
|
1611
|
+
event_type: "completion",
|
|
1612
|
+
body: JSON.stringify(eventBody)
|
|
1613
|
+
});
|
|
1614
|
+
};
|
|
1615
|
+
if (status === "cancelled") {
|
|
1616
|
+
await updateTask({
|
|
1617
|
+
id: taskId
|
|
1618
|
+
});
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (task.status === "cancelled") {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
if (!task) {
|
|
1625
|
+
throw new Error(`No task with taskId ${taskId} found`);
|
|
1626
|
+
}
|
|
1627
|
+
if (task.status !== oldStatus) {
|
|
1628
|
+
throw new errors.ConflictError(
|
|
1629
|
+
`Refusing to update status of run '${taskId}' to status '${status}' as it is currently '${task.status}', expected '${oldStatus}'`
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
await updateTask({
|
|
1633
|
+
id: taskId,
|
|
1634
|
+
status: oldStatus
|
|
1635
|
+
});
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
async emitLogEvent(options) {
|
|
1639
|
+
const { taskId, body } = options;
|
|
1640
|
+
const serializedBody = JSON.stringify(body);
|
|
1641
|
+
await this.db("task_events").insert({
|
|
1642
|
+
task_id: taskId,
|
|
1643
|
+
event_type: "log",
|
|
1644
|
+
body: serializedBody
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
async listEvents(options) {
|
|
1648
|
+
const { taskId, after } = options;
|
|
1649
|
+
const rawEvents = await this.db("task_events").where({
|
|
1650
|
+
task_id: taskId
|
|
1651
|
+
}).andWhere((builder) => {
|
|
1652
|
+
if (typeof after === "number") {
|
|
1653
|
+
builder.where("id", ">", after).orWhere("event_type", "completion");
|
|
1654
|
+
}
|
|
1655
|
+
}).orderBy("id").select();
|
|
1656
|
+
const events = rawEvents.map((event) => {
|
|
1657
|
+
try {
|
|
1658
|
+
const body = JSON.parse(event.body);
|
|
1659
|
+
return {
|
|
1660
|
+
id: Number(event.id),
|
|
1661
|
+
taskId,
|
|
1662
|
+
body,
|
|
1663
|
+
type: event.event_type,
|
|
1664
|
+
createdAt: parseSqlDateToIsoString(event.created_at)
|
|
1665
|
+
};
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
throw new Error(
|
|
1668
|
+
`Failed to parse event body from event taskId=${taskId} id=${event.id}, ${error}`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
return { events };
|
|
1673
|
+
}
|
|
1674
|
+
async shutdownTask(options) {
|
|
1675
|
+
const { taskId } = options;
|
|
1676
|
+
const message = `This task was marked as stale as it exceeded its timeout`;
|
|
1677
|
+
const statusStepEvents = (await this.listEvents({ taskId })).events.filter(
|
|
1678
|
+
({ body }) => body == null ? void 0 : body.stepId
|
|
1679
|
+
);
|
|
1680
|
+
const completedSteps = statusStepEvents.filter(
|
|
1681
|
+
({ body: { status } }) => status === "failed" || status === "completed"
|
|
1682
|
+
).map((step) => step.body.stepId);
|
|
1683
|
+
const hungProcessingSteps = statusStepEvents.filter(({ body: { status } }) => status === "processing").map((event) => event.body.stepId).filter((step) => !completedSteps.includes(step));
|
|
1684
|
+
for (const step of hungProcessingSteps) {
|
|
1685
|
+
await this.emitLogEvent({
|
|
1686
|
+
taskId,
|
|
1687
|
+
body: {
|
|
1688
|
+
message,
|
|
1689
|
+
stepId: step,
|
|
1690
|
+
status: "failed"
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
await this.completeTask({
|
|
1695
|
+
taskId,
|
|
1696
|
+
status: "failed",
|
|
1697
|
+
eventBody: {
|
|
1698
|
+
message
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
async cancelTask(options) {
|
|
1703
|
+
const { taskId, body } = options;
|
|
1704
|
+
const serializedBody = JSON.stringify(body);
|
|
1705
|
+
await this.db("task_events").insert({
|
|
1706
|
+
task_id: taskId,
|
|
1707
|
+
event_type: "cancelled",
|
|
1708
|
+
body: serializedBody
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
var __defProp$2 = Object.defineProperty;
|
|
1714
|
+
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1715
|
+
var __publicField$2 = (obj, key, value) => {
|
|
1716
|
+
__defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1717
|
+
return value;
|
|
1718
|
+
};
|
|
1719
|
+
class TaskManager {
|
|
1720
|
+
// Runs heartbeat internally
|
|
1721
|
+
constructor(task, storage, signal, logger) {
|
|
1722
|
+
this.task = task;
|
|
1723
|
+
this.storage = storage;
|
|
1724
|
+
this.signal = signal;
|
|
1725
|
+
this.logger = logger;
|
|
1726
|
+
__publicField$2(this, "isDone", false);
|
|
1727
|
+
__publicField$2(this, "heartbeatTimeoutId");
|
|
1728
|
+
}
|
|
1729
|
+
static create(task, storage, abortSignal, logger) {
|
|
1730
|
+
const agent = new TaskManager(task, storage, abortSignal, logger);
|
|
1731
|
+
agent.startTimeout();
|
|
1732
|
+
return agent;
|
|
1733
|
+
}
|
|
1734
|
+
get spec() {
|
|
1735
|
+
return this.task.spec;
|
|
1736
|
+
}
|
|
1737
|
+
get cancelSignal() {
|
|
1738
|
+
return this.signal;
|
|
1739
|
+
}
|
|
1740
|
+
get secrets() {
|
|
1741
|
+
return this.task.secrets;
|
|
1742
|
+
}
|
|
1743
|
+
get createdBy() {
|
|
1744
|
+
return this.task.createdBy;
|
|
1745
|
+
}
|
|
1746
|
+
async getWorkspaceName() {
|
|
1747
|
+
return this.task.taskId;
|
|
1748
|
+
}
|
|
1749
|
+
get done() {
|
|
1750
|
+
return this.isDone;
|
|
1751
|
+
}
|
|
1752
|
+
async emitLog(message, logMetadata) {
|
|
1753
|
+
await this.storage.emitLogEvent({
|
|
1754
|
+
taskId: this.task.taskId,
|
|
1755
|
+
body: { message, ...logMetadata }
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
async complete(result, metadata) {
|
|
1759
|
+
await this.storage.completeTask({
|
|
1760
|
+
taskId: this.task.taskId,
|
|
1761
|
+
status: result === "failed" ? "failed" : "completed",
|
|
1762
|
+
eventBody: {
|
|
1763
|
+
message: `Run completed with status: ${result}`,
|
|
1764
|
+
...metadata
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
this.isDone = true;
|
|
1768
|
+
if (this.heartbeatTimeoutId) {
|
|
1769
|
+
clearTimeout(this.heartbeatTimeoutId);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
startTimeout() {
|
|
1773
|
+
this.heartbeatTimeoutId = setTimeout(async () => {
|
|
1774
|
+
try {
|
|
1775
|
+
await this.storage.heartbeatTask(this.task.taskId);
|
|
1776
|
+
this.startTimeout();
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
this.isDone = true;
|
|
1779
|
+
this.logger.error(
|
|
1780
|
+
`Heartbeat for task ${this.task.taskId} failed`,
|
|
1781
|
+
error
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
}, 1e3);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
function defer() {
|
|
1788
|
+
let resolve = () => {
|
|
1789
|
+
};
|
|
1790
|
+
const promise = new Promise((_resolve) => {
|
|
1791
|
+
resolve = _resolve;
|
|
1792
|
+
});
|
|
1793
|
+
return { promise, resolve };
|
|
1794
|
+
}
|
|
1795
|
+
class StorageTaskBroker {
|
|
1796
|
+
constructor(storage, logger) {
|
|
1797
|
+
this.storage = storage;
|
|
1798
|
+
this.logger = logger;
|
|
1799
|
+
__publicField$2(this, "deferredDispatch", defer());
|
|
1800
|
+
}
|
|
1801
|
+
async list(options) {
|
|
1802
|
+
if (!this.storage.list) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
"TaskStore does not implement the list method. Please implement the list method to be able to list tasks"
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
return await this.storage.list({ createdBy: options == null ? void 0 : options.createdBy });
|
|
1808
|
+
}
|
|
1809
|
+
async registerCancellable(taskId, abortController) {
|
|
1810
|
+
let shouldUnsubscribe = false;
|
|
1811
|
+
const subscription = this.event$({ taskId, after: void 0 }).subscribe({
|
|
1812
|
+
error: (_) => {
|
|
1813
|
+
subscription.unsubscribe();
|
|
1814
|
+
},
|
|
1815
|
+
next: ({ events }) => {
|
|
1816
|
+
for (const event of events) {
|
|
1817
|
+
if (event.type === "cancelled") {
|
|
1818
|
+
abortController.abort();
|
|
1819
|
+
shouldUnsubscribe = true;
|
|
1820
|
+
}
|
|
1821
|
+
if (event.type === "completion") {
|
|
1822
|
+
shouldUnsubscribe = true;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (shouldUnsubscribe) {
|
|
1826
|
+
subscription.unsubscribe();
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* {@inheritdoc TaskBroker.claim}
|
|
1833
|
+
*/
|
|
1834
|
+
async claim() {
|
|
1835
|
+
for (; ; ) {
|
|
1836
|
+
const pendingTask = await this.storage.claimTask();
|
|
1837
|
+
if (pendingTask) {
|
|
1838
|
+
const abortController = new AbortController();
|
|
1839
|
+
await this.registerCancellable(pendingTask.id, abortController);
|
|
1840
|
+
return TaskManager.create(
|
|
1841
|
+
{
|
|
1842
|
+
taskId: pendingTask.id,
|
|
1843
|
+
spec: pendingTask.spec,
|
|
1844
|
+
secrets: pendingTask.secrets,
|
|
1845
|
+
createdBy: pendingTask.createdBy
|
|
1846
|
+
},
|
|
1847
|
+
this.storage,
|
|
1848
|
+
abortController.signal,
|
|
1849
|
+
this.logger
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
await this.waitForDispatch();
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* {@inheritdoc TaskBroker.dispatch}
|
|
1857
|
+
*/
|
|
1858
|
+
async dispatch(options) {
|
|
1859
|
+
const taskRow = await this.storage.createTask(options);
|
|
1860
|
+
this.signalDispatch();
|
|
1861
|
+
return {
|
|
1862
|
+
taskId: taskRow.taskId
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* {@inheritdoc TaskBroker.get}
|
|
1867
|
+
*/
|
|
1868
|
+
async get(taskId) {
|
|
1869
|
+
return this.storage.getTask(taskId);
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* {@inheritdoc TaskBroker.event$}
|
|
1873
|
+
*/
|
|
1874
|
+
event$(options) {
|
|
1875
|
+
return new ObservableImpl__default["default"]((observer) => {
|
|
1876
|
+
const { taskId } = options;
|
|
1877
|
+
let after = options.after;
|
|
1878
|
+
let cancelled = false;
|
|
1879
|
+
(async () => {
|
|
1880
|
+
while (!cancelled) {
|
|
1881
|
+
const result = await this.storage.listEvents({ taskId, after });
|
|
1882
|
+
const { events } = result;
|
|
1883
|
+
if (events.length) {
|
|
1884
|
+
after = events[events.length - 1].id;
|
|
1885
|
+
observer.next(result);
|
|
1886
|
+
}
|
|
1887
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1888
|
+
}
|
|
1889
|
+
})();
|
|
1890
|
+
return () => {
|
|
1891
|
+
cancelled = true;
|
|
1892
|
+
};
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* {@inheritdoc TaskBroker.vacuumTasks}
|
|
1897
|
+
*/
|
|
1898
|
+
async vacuumTasks(options) {
|
|
1899
|
+
const { tasks } = await this.storage.listStaleTasks(options);
|
|
1900
|
+
await Promise.all(
|
|
1901
|
+
tasks.map(async (task) => {
|
|
1902
|
+
try {
|
|
1903
|
+
await this.storage.completeTask({
|
|
1904
|
+
taskId: task.taskId,
|
|
1905
|
+
status: "failed",
|
|
1906
|
+
eventBody: {
|
|
1907
|
+
message: "The task was cancelled because the task worker lost connection to the task broker"
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
this.logger.warn(`Failed to cancel task '${task.taskId}', ${error}`);
|
|
1912
|
+
}
|
|
1913
|
+
})
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
waitForDispatch() {
|
|
1917
|
+
return this.deferredDispatch.promise;
|
|
1918
|
+
}
|
|
1919
|
+
signalDispatch() {
|
|
1920
|
+
this.deferredDispatch.resolve();
|
|
1921
|
+
this.deferredDispatch = defer();
|
|
1922
|
+
}
|
|
1923
|
+
async cancel(taskId) {
|
|
1924
|
+
var _a, _b;
|
|
1925
|
+
const { events } = await this.storage.listEvents({ taskId });
|
|
1926
|
+
const currentStepId = events.length > 0 ? events.filter(({ body }) => body == null ? void 0 : body.stepId).reduce((prev, curr) => prev.id > curr.id ? prev : curr).body.stepId : 0;
|
|
1927
|
+
await ((_b = (_a = this.storage).cancelTask) == null ? void 0 : _b.call(_a, {
|
|
1928
|
+
taskId,
|
|
1929
|
+
body: {
|
|
1930
|
+
message: `Step ${currentStepId} has been cancelled.`,
|
|
1931
|
+
stepId: currentStepId,
|
|
1932
|
+
status: "cancelled"
|
|
1933
|
+
}
|
|
1934
|
+
}));
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function isTruthy(value) {
|
|
1939
|
+
return lodash.isArray(value) ? value.length > 0 : !!value;
|
|
1940
|
+
}
|
|
1941
|
+
function generateExampleOutput(schema) {
|
|
1942
|
+
var _a, _b;
|
|
1943
|
+
const { examples } = schema;
|
|
1944
|
+
if (examples && Array.isArray(examples)) {
|
|
1945
|
+
return examples[0];
|
|
1946
|
+
}
|
|
1947
|
+
if (schema.type === "object") {
|
|
1948
|
+
return Object.fromEntries(
|
|
1949
|
+
Object.entries((_a = schema.properties) != null ? _a : {}).map(([key, value]) => [
|
|
1950
|
+
key,
|
|
1951
|
+
generateExampleOutput(value)
|
|
1952
|
+
])
|
|
1953
|
+
);
|
|
1954
|
+
} else if (schema.type === "array") {
|
|
1955
|
+
const [firstSchema] = (_b = [schema.items]) == null ? void 0 : _b.flat();
|
|
1956
|
+
if (firstSchema) {
|
|
1957
|
+
return [generateExampleOutput(firstSchema)];
|
|
1958
|
+
}
|
|
1959
|
+
return [];
|
|
1960
|
+
} else if (schema.type === "string") {
|
|
1961
|
+
return "<example>";
|
|
1962
|
+
} else if (schema.type === "number") {
|
|
1963
|
+
return 0;
|
|
1964
|
+
} else if (schema.type === "boolean") {
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
return "<unknown>";
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function createCounterMetric(config) {
|
|
1971
|
+
let metric = promClient.register.getSingleMetric(config.name);
|
|
1972
|
+
if (!metric) {
|
|
1973
|
+
metric = new promClient.Counter(config);
|
|
1974
|
+
promClient.register.registerMetric(metric);
|
|
1975
|
+
}
|
|
1976
|
+
return metric;
|
|
1977
|
+
}
|
|
1978
|
+
function createHistogramMetric(config) {
|
|
1979
|
+
let metric = promClient.register.getSingleMetric(config.name);
|
|
1980
|
+
if (!metric) {
|
|
1981
|
+
metric = new promClient.Histogram(config);
|
|
1982
|
+
promClient.register.registerMetric(metric);
|
|
1983
|
+
}
|
|
1984
|
+
return metric;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
const createTemplatePermissionRule = pluginPermissionNode.makeCreatePermissionRule();
|
|
1988
|
+
const hasTag = createTemplatePermissionRule({
|
|
1989
|
+
name: "HAS_TAG",
|
|
1990
|
+
resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
|
1991
|
+
description: `Match parameters or steps with the given tag`,
|
|
1992
|
+
paramsSchema: zod.z.object({
|
|
1993
|
+
tag: zod.z.string().describe("Name of the tag to match on")
|
|
1994
|
+
}),
|
|
1995
|
+
apply: (resource, { tag }) => {
|
|
1996
|
+
var _a, _b, _c;
|
|
1997
|
+
return (_c = (_b = (_a = resource["backstage:permissions"]) == null ? void 0 : _a.tags) == null ? void 0 : _b.includes(tag)) != null ? _c : false;
|
|
1998
|
+
},
|
|
1999
|
+
toQuery: () => ({})
|
|
2000
|
+
});
|
|
2001
|
+
const createActionPermissionRule = pluginPermissionNode.makeCreatePermissionRule();
|
|
2002
|
+
const hasActionId = createActionPermissionRule({
|
|
2003
|
+
name: "HAS_ACTION_ID",
|
|
2004
|
+
resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION,
|
|
2005
|
+
description: `Match actions with the given actionId`,
|
|
2006
|
+
paramsSchema: zod.z.object({
|
|
2007
|
+
actionId: zod.z.string().describe("Name of the actionId to match on")
|
|
2008
|
+
}),
|
|
2009
|
+
apply: (resource, { actionId }) => {
|
|
2010
|
+
return resource.action === actionId;
|
|
2011
|
+
},
|
|
2012
|
+
toQuery: () => ({})
|
|
2013
|
+
});
|
|
2014
|
+
buildHasProperty({
|
|
2015
|
+
name: "HAS_PROPERTY",
|
|
2016
|
+
valueSchema: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]),
|
|
2017
|
+
validateProperty: false
|
|
2018
|
+
});
|
|
2019
|
+
const hasBooleanProperty = buildHasProperty({
|
|
2020
|
+
name: "HAS_BOOLEAN_PROPERTY",
|
|
2021
|
+
valueSchema: zod.z.boolean()
|
|
2022
|
+
});
|
|
2023
|
+
const hasNumberProperty = buildHasProperty({
|
|
2024
|
+
name: "HAS_NUMBER_PROPERTY",
|
|
2025
|
+
valueSchema: zod.z.number()
|
|
2026
|
+
});
|
|
2027
|
+
const hasStringProperty = buildHasProperty({
|
|
2028
|
+
name: "HAS_STRING_PROPERTY",
|
|
2029
|
+
valueSchema: zod.z.string()
|
|
2030
|
+
});
|
|
2031
|
+
function buildHasProperty({
|
|
2032
|
+
name,
|
|
2033
|
+
valueSchema,
|
|
2034
|
+
validateProperty = true
|
|
2035
|
+
}) {
|
|
2036
|
+
return createActionPermissionRule({
|
|
2037
|
+
name,
|
|
2038
|
+
description: `Allow actions with the specified property`,
|
|
2039
|
+
resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION,
|
|
2040
|
+
paramsSchema: zod.z.object({
|
|
2041
|
+
key: zod.z.string().describe(`Property within the action parameters to match on`),
|
|
2042
|
+
value: valueSchema.describe(`Value of the given property to match on`)
|
|
2043
|
+
}),
|
|
2044
|
+
apply: (resource, { key, value }) => {
|
|
2045
|
+
const foundValue = lodash.get(resource.input, key);
|
|
2046
|
+
if (validateProperty && !valueSchema.safeParse(foundValue).success) {
|
|
2047
|
+
return false;
|
|
2048
|
+
}
|
|
2049
|
+
if (value !== void 0) {
|
|
2050
|
+
if (valueSchema.safeParse(value).success) {
|
|
2051
|
+
return value === foundValue;
|
|
2052
|
+
}
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2055
|
+
return foundValue !== void 0;
|
|
2056
|
+
},
|
|
2057
|
+
toQuery: () => ({})
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
const scaffolderTemplateRules = { hasTag };
|
|
2061
|
+
const scaffolderActionRules = {
|
|
2062
|
+
hasActionId,
|
|
2063
|
+
hasBooleanProperty,
|
|
2064
|
+
hasNumberProperty,
|
|
2065
|
+
hasStringProperty
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
var __defProp$1 = Object.defineProperty;
|
|
2069
|
+
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
2070
|
+
var __publicField$1 = (obj, key, value) => {
|
|
2071
|
+
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
2072
|
+
return value;
|
|
2073
|
+
};
|
|
2074
|
+
const isValidTaskSpec = (taskSpec) => {
|
|
2075
|
+
return taskSpec.apiVersion === "scaffolder.backstage.io/v1beta3";
|
|
2076
|
+
};
|
|
2077
|
+
const createStepLogger = ({
|
|
2078
|
+
task,
|
|
2079
|
+
step
|
|
2080
|
+
}) => {
|
|
2081
|
+
const metadata = { stepId: step.id };
|
|
2082
|
+
const taskLogger = winston__namespace.createLogger({
|
|
2083
|
+
level: process.env.LOG_LEVEL || "info",
|
|
2084
|
+
format: winston__namespace.format.combine(
|
|
2085
|
+
winston__namespace.format.colorize(),
|
|
2086
|
+
winston__namespace.format.simple()
|
|
2087
|
+
),
|
|
2088
|
+
defaultMeta: {}
|
|
2089
|
+
});
|
|
2090
|
+
const streamLogger = new stream.PassThrough();
|
|
2091
|
+
streamLogger.on("data", async (data) => {
|
|
2092
|
+
const message = data.toString().trim();
|
|
2093
|
+
if ((message == null ? void 0 : message.length) > 1) {
|
|
2094
|
+
await task.emitLog(message, metadata);
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
taskLogger.add(new winston__namespace.transports.Stream({ stream: streamLogger }));
|
|
2098
|
+
return { taskLogger, streamLogger };
|
|
2099
|
+
};
|
|
2100
|
+
const isActionAuthorized = pluginPermissionNode.createConditionAuthorizer(
|
|
2101
|
+
Object.values(scaffolderActionRules)
|
|
2102
|
+
);
|
|
2103
|
+
class NunjucksWorkflowRunner {
|
|
2104
|
+
constructor(options) {
|
|
2105
|
+
this.options = options;
|
|
2106
|
+
__publicField$1(this, "defaultTemplateFilters");
|
|
2107
|
+
__publicField$1(this, "tracker", scaffoldingTracker());
|
|
2108
|
+
this.defaultTemplateFilters = createDefaultFilters({
|
|
2109
|
+
integrations: this.options.integrations
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
isSingleTemplateString(input) {
|
|
2113
|
+
var _a, _b;
|
|
2114
|
+
const { parser, nodes } = nunjucks__default["default"];
|
|
2115
|
+
const parsed = parser.parse(
|
|
2116
|
+
input,
|
|
2117
|
+
{},
|
|
2118
|
+
{
|
|
2119
|
+
autoescape: false,
|
|
2120
|
+
tags: {
|
|
2121
|
+
variableStart: "${{",
|
|
2122
|
+
variableEnd: "}}"
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
);
|
|
2126
|
+
return parsed.children.length === 1 && !(((_b = (_a = parsed.children[0]) == null ? void 0 : _a.children) == null ? void 0 : _b[0]) instanceof nodes.TemplateData);
|
|
2127
|
+
}
|
|
2128
|
+
render(input, context, renderTemplate) {
|
|
2129
|
+
return JSON.parse(JSON.stringify(input), (_key, value) => {
|
|
2130
|
+
try {
|
|
2131
|
+
if (typeof value === "string") {
|
|
2132
|
+
try {
|
|
2133
|
+
if (this.isSingleTemplateString(value)) {
|
|
2134
|
+
const wrappedDumped = value.replace(
|
|
2135
|
+
/\${{(.+)}}/g,
|
|
2136
|
+
"${{ ( $1 ) | dump }}"
|
|
2137
|
+
);
|
|
2138
|
+
const templated2 = renderTemplate(wrappedDumped, context);
|
|
2139
|
+
if (templated2 === "") {
|
|
2140
|
+
return void 0;
|
|
2141
|
+
}
|
|
2142
|
+
return JSON.parse(templated2);
|
|
2143
|
+
}
|
|
2144
|
+
} catch (ex) {
|
|
2145
|
+
this.options.logger.error(
|
|
2146
|
+
`Failed to parse template string: ${value} with error ${ex.message}`
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
const templated = renderTemplate(value, context);
|
|
2150
|
+
if (templated === "") {
|
|
2151
|
+
return void 0;
|
|
2152
|
+
}
|
|
2153
|
+
return templated;
|
|
2154
|
+
}
|
|
2155
|
+
} catch {
|
|
2156
|
+
return value;
|
|
2157
|
+
}
|
|
2158
|
+
return value;
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
async executeStep(task, step, context, renderTemplate, taskTrack, workspacePath, decision) {
|
|
2162
|
+
var _a, _b, _c, _d, _e;
|
|
2163
|
+
const stepTrack = await this.tracker.stepStart(task, step);
|
|
2164
|
+
if (task.cancelSignal.aborted) {
|
|
2165
|
+
throw new Error(`Step ${step.name} has been cancelled.`);
|
|
2166
|
+
}
|
|
2167
|
+
try {
|
|
2168
|
+
if (step.if) {
|
|
2169
|
+
const ifResult = await this.render(step.if, context, renderTemplate);
|
|
2170
|
+
if (!isTruthy(ifResult)) {
|
|
2171
|
+
await stepTrack.skipFalsy();
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const action = this.options.actionRegistry.get(step.action);
|
|
2176
|
+
const { taskLogger, streamLogger } = createStepLogger({ task, step });
|
|
2177
|
+
if (task.isDryRun) {
|
|
2178
|
+
const redactedSecrets = Object.fromEntries(
|
|
2179
|
+
Object.entries((_a = task.secrets) != null ? _a : {}).map((secret) => [
|
|
2180
|
+
secret[0],
|
|
2181
|
+
"[REDACTED]"
|
|
2182
|
+
])
|
|
2183
|
+
);
|
|
2184
|
+
const debugInput = (_b = step.input && this.render(
|
|
2185
|
+
step.input,
|
|
2186
|
+
{
|
|
2187
|
+
...context,
|
|
2188
|
+
secrets: redactedSecrets
|
|
2189
|
+
},
|
|
2190
|
+
renderTemplate
|
|
2191
|
+
)) != null ? _b : {};
|
|
2192
|
+
taskLogger.info(
|
|
2193
|
+
`Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
|
|
2194
|
+
debugInput,
|
|
2195
|
+
void 0,
|
|
2196
|
+
2
|
|
2197
|
+
)}`
|
|
2198
|
+
);
|
|
2199
|
+
if (!action.supportsDryRun) {
|
|
2200
|
+
await taskTrack.skipDryRun(step, action);
|
|
2201
|
+
const outputSchema = (_c = action.schema) == null ? void 0 : _c.output;
|
|
2202
|
+
if (outputSchema) {
|
|
2203
|
+
context.steps[step.id] = {
|
|
2204
|
+
output: generateExampleOutput(outputSchema)
|
|
2205
|
+
};
|
|
2206
|
+
} else {
|
|
2207
|
+
context.steps[step.id] = { output: {} };
|
|
2208
|
+
}
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
const iterations = (step.each ? Object.entries(this.render(step.each, context, renderTemplate)).map(
|
|
2213
|
+
([key, value]) => ({
|
|
2214
|
+
each: { key, value }
|
|
2215
|
+
})
|
|
2216
|
+
) : [{}]).map((i) => {
|
|
2217
|
+
var _a2;
|
|
2218
|
+
return {
|
|
2219
|
+
...i,
|
|
2220
|
+
// Secrets are only passed when templating the input to actions for security reasons
|
|
2221
|
+
input: step.input ? this.render(
|
|
2222
|
+
step.input,
|
|
2223
|
+
{ ...context, secrets: (_a2 = task.secrets) != null ? _a2 : {}, ...i },
|
|
2224
|
+
renderTemplate
|
|
2225
|
+
) : {}
|
|
2226
|
+
};
|
|
2227
|
+
});
|
|
2228
|
+
for (const iteration of iterations) {
|
|
2229
|
+
const actionId = `${action.id}${iteration.each ? `[${iteration.each.key}]` : ""}`;
|
|
2230
|
+
if ((_d = action.schema) == null ? void 0 : _d.input) {
|
|
2231
|
+
const validateResult = jsonschema.validate(
|
|
2232
|
+
iteration.input,
|
|
2233
|
+
action.schema.input
|
|
2234
|
+
);
|
|
2235
|
+
if (!validateResult.valid) {
|
|
2236
|
+
const errors$1 = validateResult.errors.join(", ");
|
|
2237
|
+
throw new errors.InputError(
|
|
2238
|
+
`Invalid input passed to action ${actionId}, ${errors$1}`
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
if (!isActionAuthorized(decision, {
|
|
2243
|
+
action: action.id,
|
|
2244
|
+
input: iteration.input
|
|
2245
|
+
})) {
|
|
2246
|
+
throw new errors.NotAllowedError(
|
|
2247
|
+
`Unauthorized action: ${actionId}. The action is not allowed. Input: ${JSON.stringify(
|
|
2248
|
+
iteration.input,
|
|
2249
|
+
null,
|
|
2250
|
+
2
|
|
2251
|
+
)}`
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
const tmpDirs = new Array();
|
|
2256
|
+
const stepOutput = {};
|
|
2257
|
+
for (const iteration of iterations) {
|
|
2258
|
+
if (iteration.each) {
|
|
2259
|
+
taskLogger.info(
|
|
2260
|
+
`Running step each: ${JSON.stringify(
|
|
2261
|
+
iteration.each,
|
|
2262
|
+
(k, v) => k ? v.toString() : v,
|
|
2263
|
+
0
|
|
2264
|
+
)}`
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
await action.handler({
|
|
2268
|
+
input: iteration.input,
|
|
2269
|
+
secrets: (_e = task.secrets) != null ? _e : {},
|
|
2270
|
+
logger: taskLogger,
|
|
2271
|
+
logStream: streamLogger,
|
|
2272
|
+
workspacePath,
|
|
2273
|
+
createTemporaryDirectory: async () => {
|
|
2274
|
+
const tmpDir = await fs__default["default"].mkdtemp(
|
|
2275
|
+
`${workspacePath}_step-${step.id}-`
|
|
2276
|
+
);
|
|
2277
|
+
tmpDirs.push(tmpDir);
|
|
2278
|
+
return tmpDir;
|
|
2279
|
+
},
|
|
2280
|
+
output(name, value) {
|
|
2281
|
+
if (step.each) {
|
|
2282
|
+
stepOutput[name] = stepOutput[name] || [];
|
|
2283
|
+
stepOutput[name].push(value);
|
|
2284
|
+
} else {
|
|
2285
|
+
stepOutput[name] = value;
|
|
2286
|
+
}
|
|
2287
|
+
},
|
|
2288
|
+
templateInfo: task.spec.templateInfo,
|
|
2289
|
+
user: task.spec.user,
|
|
2290
|
+
isDryRun: task.isDryRun,
|
|
2291
|
+
signal: task.cancelSignal
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
for (const tmpDir of tmpDirs) {
|
|
2295
|
+
await fs__default["default"].remove(tmpDir);
|
|
2296
|
+
}
|
|
2297
|
+
context.steps[step.id] = { output: stepOutput };
|
|
2298
|
+
if (task.cancelSignal.aborted) {
|
|
2299
|
+
throw new Error(`Step ${step.name} has been cancelled.`);
|
|
2300
|
+
}
|
|
2301
|
+
await stepTrack.markSuccessful();
|
|
2302
|
+
} catch (err) {
|
|
2303
|
+
await taskTrack.markFailed(step, err);
|
|
2304
|
+
await stepTrack.markFailed();
|
|
2305
|
+
throw err;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async execute(task) {
|
|
2309
|
+
var _a;
|
|
2310
|
+
if (!isValidTaskSpec(task.spec)) {
|
|
2311
|
+
throw new errors.InputError(
|
|
2312
|
+
"Wrong template version executed with the workflow engine"
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
const workspacePath = path__default["default"].join(
|
|
2316
|
+
this.options.workingDirectory,
|
|
2317
|
+
await task.getWorkspaceName()
|
|
2318
|
+
);
|
|
2319
|
+
const { additionalTemplateFilters, additionalTemplateGlobals } = this.options;
|
|
2320
|
+
const renderTemplate = await SecureTemplater.loadRenderer({
|
|
2321
|
+
templateFilters: {
|
|
2322
|
+
...this.defaultTemplateFilters,
|
|
2323
|
+
...additionalTemplateFilters
|
|
2324
|
+
},
|
|
2325
|
+
templateGlobals: additionalTemplateGlobals
|
|
2326
|
+
});
|
|
2327
|
+
try {
|
|
2328
|
+
const taskTrack = await this.tracker.taskStart(task);
|
|
2329
|
+
await fs__default["default"].ensureDir(workspacePath);
|
|
2330
|
+
const context = {
|
|
2331
|
+
parameters: task.spec.parameters,
|
|
2332
|
+
steps: {},
|
|
2333
|
+
user: task.spec.user
|
|
2334
|
+
};
|
|
2335
|
+
const [decision] = this.options.permissions && task.spec.steps.length ? await this.options.permissions.authorizeConditional(
|
|
2336
|
+
[{ permission: alpha.actionExecutePermission }],
|
|
2337
|
+
{ token: (_a = task.secrets) == null ? void 0 : _a.backstageToken }
|
|
2338
|
+
) : [{ result: pluginPermissionCommon.AuthorizeResult.ALLOW }];
|
|
2339
|
+
for (const step of task.spec.steps) {
|
|
2340
|
+
await this.executeStep(
|
|
2341
|
+
task,
|
|
2342
|
+
step,
|
|
2343
|
+
context,
|
|
2344
|
+
renderTemplate,
|
|
2345
|
+
taskTrack,
|
|
2346
|
+
workspacePath,
|
|
2347
|
+
decision
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
const output = this.render(task.spec.output, context, renderTemplate);
|
|
2351
|
+
await taskTrack.markSuccessful();
|
|
2352
|
+
return { output };
|
|
2353
|
+
} finally {
|
|
2354
|
+
if (workspacePath) {
|
|
2355
|
+
await fs__default["default"].remove(workspacePath);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
function scaffoldingTracker() {
|
|
2361
|
+
const taskCount = createCounterMetric({
|
|
2362
|
+
name: "scaffolder_task_count",
|
|
2363
|
+
help: "Count of task runs",
|
|
2364
|
+
labelNames: ["template", "user", "result"]
|
|
2365
|
+
});
|
|
2366
|
+
const taskDuration = createHistogramMetric({
|
|
2367
|
+
name: "scaffolder_task_duration",
|
|
2368
|
+
help: "Duration of a task run",
|
|
2369
|
+
labelNames: ["template", "result"]
|
|
2370
|
+
});
|
|
2371
|
+
const stepCount = createCounterMetric({
|
|
2372
|
+
name: "scaffolder_step_count",
|
|
2373
|
+
help: "Count of step runs",
|
|
2374
|
+
labelNames: ["template", "step", "result"]
|
|
2375
|
+
});
|
|
2376
|
+
const stepDuration = createHistogramMetric({
|
|
2377
|
+
name: "scaffolder_step_duration",
|
|
2378
|
+
help: "Duration of a step runs",
|
|
2379
|
+
labelNames: ["template", "step", "result"]
|
|
2380
|
+
});
|
|
2381
|
+
async function taskStart(task) {
|
|
2382
|
+
var _a, _b;
|
|
2383
|
+
await task.emitLog(`Starting up task with ${task.spec.steps.length} steps`);
|
|
2384
|
+
const template = ((_a = task.spec.templateInfo) == null ? void 0 : _a.entityRef) || "";
|
|
2385
|
+
const user = ((_b = task.spec.user) == null ? void 0 : _b.ref) || "";
|
|
2386
|
+
const taskTimer = taskDuration.startTimer({
|
|
2387
|
+
template
|
|
2388
|
+
});
|
|
2389
|
+
async function skipDryRun(step, action) {
|
|
2390
|
+
task.emitLog(`Skipping because ${action.id} does not support dry-run`, {
|
|
2391
|
+
stepId: step.id,
|
|
2392
|
+
status: "skipped"
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
async function markSuccessful() {
|
|
2396
|
+
taskCount.inc({
|
|
2397
|
+
template,
|
|
2398
|
+
user,
|
|
2399
|
+
result: "ok"
|
|
2400
|
+
});
|
|
2401
|
+
taskTimer({ result: "ok" });
|
|
2402
|
+
}
|
|
2403
|
+
async function markFailed(step, err) {
|
|
2404
|
+
await task.emitLog(String(err.stack), {
|
|
2405
|
+
stepId: step.id,
|
|
2406
|
+
status: "failed"
|
|
2407
|
+
});
|
|
2408
|
+
taskCount.inc({
|
|
2409
|
+
template,
|
|
2410
|
+
user,
|
|
2411
|
+
result: "failed"
|
|
2412
|
+
});
|
|
2413
|
+
taskTimer({ result: "failed" });
|
|
2414
|
+
}
|
|
2415
|
+
async function markCancelled(step) {
|
|
2416
|
+
await task.emitLog(`Step ${step.id} has been cancelled.`, {
|
|
2417
|
+
stepId: step.id,
|
|
2418
|
+
status: "cancelled"
|
|
2419
|
+
});
|
|
2420
|
+
taskCount.inc({
|
|
2421
|
+
template,
|
|
2422
|
+
user,
|
|
2423
|
+
result: "cancelled"
|
|
2424
|
+
});
|
|
2425
|
+
taskTimer({ result: "cancelled" });
|
|
2426
|
+
}
|
|
2427
|
+
return {
|
|
2428
|
+
skipDryRun,
|
|
2429
|
+
markCancelled,
|
|
2430
|
+
markSuccessful,
|
|
2431
|
+
markFailed
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
async function stepStart(task, step) {
|
|
2435
|
+
var _a;
|
|
2436
|
+
await task.emitLog(`Beginning step ${step.name}`, {
|
|
2437
|
+
stepId: step.id,
|
|
2438
|
+
status: "processing"
|
|
2439
|
+
});
|
|
2440
|
+
const template = ((_a = task.spec.templateInfo) == null ? void 0 : _a.entityRef) || "";
|
|
2441
|
+
const stepTimer = stepDuration.startTimer({
|
|
2442
|
+
template,
|
|
2443
|
+
step: step.name
|
|
2444
|
+
});
|
|
2445
|
+
async function markSuccessful() {
|
|
2446
|
+
await task.emitLog(`Finished step ${step.name}`, {
|
|
2447
|
+
stepId: step.id,
|
|
2448
|
+
status: "completed"
|
|
2449
|
+
});
|
|
2450
|
+
stepCount.inc({
|
|
2451
|
+
template,
|
|
2452
|
+
step: step.name,
|
|
2453
|
+
result: "ok"
|
|
2454
|
+
});
|
|
2455
|
+
stepTimer({ result: "ok" });
|
|
2456
|
+
}
|
|
2457
|
+
async function markCancelled() {
|
|
2458
|
+
stepCount.inc({
|
|
2459
|
+
template,
|
|
2460
|
+
step: step.name,
|
|
2461
|
+
result: "cancelled"
|
|
2462
|
+
});
|
|
2463
|
+
stepTimer({ result: "cancelled" });
|
|
2464
|
+
}
|
|
2465
|
+
async function markFailed() {
|
|
2466
|
+
stepCount.inc({
|
|
2467
|
+
template,
|
|
2468
|
+
step: step.name,
|
|
2469
|
+
result: "failed"
|
|
2470
|
+
});
|
|
2471
|
+
stepTimer({ result: "failed" });
|
|
2472
|
+
}
|
|
2473
|
+
async function skipFalsy() {
|
|
2474
|
+
await task.emitLog(
|
|
2475
|
+
`Skipping step ${step.id} because its if condition was false`,
|
|
2476
|
+
{ stepId: step.id, status: "skipped" }
|
|
2477
|
+
);
|
|
2478
|
+
stepTimer({ result: "skipped" });
|
|
2479
|
+
}
|
|
2480
|
+
return {
|
|
2481
|
+
markCancelled,
|
|
2482
|
+
markFailed,
|
|
2483
|
+
markSuccessful,
|
|
2484
|
+
skipFalsy
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
return {
|
|
2488
|
+
taskStart,
|
|
2489
|
+
stepStart
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
var __defProp = Object.defineProperty;
|
|
2494
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
2495
|
+
var __publicField = (obj, key, value) => {
|
|
2496
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
2497
|
+
return value;
|
|
2498
|
+
};
|
|
2499
|
+
class TaskWorker {
|
|
2500
|
+
constructor(options) {
|
|
2501
|
+
this.options = options;
|
|
2502
|
+
__publicField(this, "taskQueue");
|
|
2503
|
+
this.taskQueue = new PQueue__default["default"]({
|
|
2504
|
+
concurrency: options.concurrentTasksLimit
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
static async create(options) {
|
|
2508
|
+
const {
|
|
2509
|
+
taskBroker,
|
|
2510
|
+
logger,
|
|
2511
|
+
actionRegistry,
|
|
2512
|
+
integrations,
|
|
2513
|
+
workingDirectory,
|
|
2514
|
+
additionalTemplateFilters,
|
|
2515
|
+
concurrentTasksLimit = 10,
|
|
2516
|
+
// from 1 to Infinity
|
|
2517
|
+
additionalTemplateGlobals,
|
|
2518
|
+
permissions
|
|
2519
|
+
} = options;
|
|
2520
|
+
const workflowRunner = new NunjucksWorkflowRunner({
|
|
2521
|
+
actionRegistry,
|
|
2522
|
+
integrations,
|
|
2523
|
+
logger,
|
|
2524
|
+
workingDirectory,
|
|
2525
|
+
additionalTemplateFilters,
|
|
2526
|
+
additionalTemplateGlobals,
|
|
2527
|
+
permissions
|
|
2528
|
+
});
|
|
2529
|
+
return new TaskWorker({
|
|
2530
|
+
taskBroker,
|
|
2531
|
+
runners: { workflowRunner },
|
|
2532
|
+
concurrentTasksLimit,
|
|
2533
|
+
permissions
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
start() {
|
|
2537
|
+
(async () => {
|
|
2538
|
+
for (; ; ) {
|
|
2539
|
+
await this.onReadyToClaimTask();
|
|
2540
|
+
const task = await this.options.taskBroker.claim();
|
|
2541
|
+
this.taskQueue.add(() => this.runOneTask(task));
|
|
2542
|
+
}
|
|
2543
|
+
})();
|
|
2544
|
+
}
|
|
2545
|
+
onReadyToClaimTask() {
|
|
2546
|
+
if (this.taskQueue.pending < this.options.concurrentTasksLimit) {
|
|
2547
|
+
return Promise.resolve();
|
|
2548
|
+
}
|
|
2549
|
+
return new Promise((resolve) => {
|
|
2550
|
+
this.taskQueue.once("next", () => {
|
|
2551
|
+
resolve();
|
|
2552
|
+
});
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
async runOneTask(task) {
|
|
2556
|
+
try {
|
|
2557
|
+
if (task.spec.apiVersion !== "scaffolder.backstage.io/v1beta3") {
|
|
2558
|
+
throw new Error(
|
|
2559
|
+
`Unsupported Template apiVersion ${task.spec.apiVersion}`
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
const { output } = await this.options.runners.workflowRunner.execute(
|
|
2563
|
+
task
|
|
2564
|
+
);
|
|
2565
|
+
await task.complete("completed", { output });
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
errors.assertError(error);
|
|
2568
|
+
await task.complete("failed", {
|
|
2569
|
+
error: { name: error.name, message: error.message }
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
class DecoratedActionsRegistry extends TemplateActionRegistry {
|
|
2576
|
+
constructor(innerRegistry, extraActions) {
|
|
2577
|
+
super();
|
|
2578
|
+
this.innerRegistry = innerRegistry;
|
|
2579
|
+
for (const action of extraActions) {
|
|
2580
|
+
this.register(action);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
get(actionId) {
|
|
2584
|
+
try {
|
|
2585
|
+
return super.get(actionId);
|
|
2586
|
+
} catch {
|
|
2587
|
+
return this.innerRegistry.get(actionId);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
function createDryRunner(options) {
|
|
2593
|
+
return async function dryRun(input) {
|
|
2594
|
+
let contentPromise;
|
|
2595
|
+
const workflowRunner = new NunjucksWorkflowRunner({
|
|
2596
|
+
...options,
|
|
2597
|
+
actionRegistry: new DecoratedActionsRegistry(options.actionRegistry, [
|
|
2598
|
+
pluginScaffolderNode.createTemplateAction({
|
|
2599
|
+
id: "dry-run:extract",
|
|
2600
|
+
supportsDryRun: true,
|
|
2601
|
+
async handler(ctx) {
|
|
2602
|
+
contentPromise = pluginScaffolderNode.serializeDirectoryContents(ctx.workspacePath);
|
|
2603
|
+
await contentPromise.catch(() => {
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
})
|
|
2607
|
+
])
|
|
2608
|
+
});
|
|
2609
|
+
const dryRunId = uuid.v4();
|
|
2610
|
+
const log = new Array();
|
|
2611
|
+
const contentsPath = backendCommon.resolveSafeChildPath(
|
|
2612
|
+
options.workingDirectory,
|
|
2613
|
+
`dry-run-content-${dryRunId}`
|
|
2614
|
+
);
|
|
2615
|
+
try {
|
|
2616
|
+
await pluginScaffolderNode.deserializeDirectoryContents(contentsPath, input.directoryContents);
|
|
2617
|
+
const abortSignal = new AbortController().signal;
|
|
2618
|
+
const result = await workflowRunner.execute({
|
|
2619
|
+
spec: {
|
|
2620
|
+
...input.spec,
|
|
2621
|
+
steps: [
|
|
2622
|
+
...input.spec.steps,
|
|
2623
|
+
{
|
|
2624
|
+
id: dryRunId,
|
|
2625
|
+
name: "dry-run:extract",
|
|
2626
|
+
action: "dry-run:extract"
|
|
2627
|
+
}
|
|
2628
|
+
],
|
|
2629
|
+
templateInfo: {
|
|
2630
|
+
entityRef: "template:default/dry-run",
|
|
2631
|
+
baseUrl: url.pathToFileURL(
|
|
2632
|
+
backendCommon.resolveSafeChildPath(contentsPath, "template.yaml")
|
|
2633
|
+
).toString()
|
|
2634
|
+
}
|
|
2635
|
+
},
|
|
2636
|
+
secrets: input.secrets,
|
|
2637
|
+
// No need to update this at the end of the run, so just hard-code it
|
|
2638
|
+
done: false,
|
|
2639
|
+
isDryRun: true,
|
|
2640
|
+
getWorkspaceName: async () => `dry-run-${dryRunId}`,
|
|
2641
|
+
cancelSignal: abortSignal,
|
|
2642
|
+
async emitLog(message, logMetadata) {
|
|
2643
|
+
if ((logMetadata == null ? void 0 : logMetadata.stepId) === dryRunId) {
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
log.push({
|
|
2647
|
+
body: {
|
|
2648
|
+
...logMetadata,
|
|
2649
|
+
message
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2652
|
+
},
|
|
2653
|
+
complete: async () => {
|
|
2654
|
+
throw new Error("Not implemented");
|
|
2655
|
+
}
|
|
2656
|
+
});
|
|
2657
|
+
if (!contentPromise) {
|
|
2658
|
+
throw new Error("Content extraction step was skipped");
|
|
2659
|
+
}
|
|
2660
|
+
const directoryContents = await contentPromise;
|
|
2661
|
+
return {
|
|
2662
|
+
log,
|
|
2663
|
+
directoryContents,
|
|
2664
|
+
output: result.output
|
|
2665
|
+
};
|
|
2666
|
+
} finally {
|
|
2667
|
+
await fs__default["default"].remove(contentsPath);
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
async function getWorkingDirectory(config, logger) {
|
|
2673
|
+
if (!config.has("backend.workingDirectory")) {
|
|
2674
|
+
return os__default["default"].tmpdir();
|
|
2675
|
+
}
|
|
2676
|
+
const workingDirectory = config.getString("backend.workingDirectory");
|
|
2677
|
+
try {
|
|
2678
|
+
await fs__default["default"].access(workingDirectory, fs__default["default"].constants.F_OK | fs__default["default"].constants.W_OK);
|
|
2679
|
+
logger.info(`using working directory: ${workingDirectory}`);
|
|
2680
|
+
} catch (err) {
|
|
2681
|
+
errors.assertError(err);
|
|
2682
|
+
logger.error(
|
|
2683
|
+
`working directory ${workingDirectory} ${err.code === "ENOENT" ? "does not exist" : "is not writable"}`
|
|
2684
|
+
);
|
|
2685
|
+
throw err;
|
|
2686
|
+
}
|
|
2687
|
+
return workingDirectory;
|
|
2688
|
+
}
|
|
2689
|
+
function getEntityBaseUrl(entity) {
|
|
2690
|
+
var _a, _b;
|
|
2691
|
+
let location = (_a = entity.metadata.annotations) == null ? void 0 : _a[catalogModel.ANNOTATION_SOURCE_LOCATION];
|
|
2692
|
+
if (!location) {
|
|
2693
|
+
location = (_b = entity.metadata.annotations) == null ? void 0 : _b[catalogModel.ANNOTATION_LOCATION];
|
|
2694
|
+
}
|
|
2695
|
+
if (!location) {
|
|
2696
|
+
return void 0;
|
|
2697
|
+
}
|
|
2698
|
+
const { type, target } = catalogModel.parseLocationRef(location);
|
|
2699
|
+
if (type === "url") {
|
|
2700
|
+
return target;
|
|
2701
|
+
} else if (type === "file") {
|
|
2702
|
+
return `file://${target}`;
|
|
2703
|
+
}
|
|
2704
|
+
return void 0;
|
|
2705
|
+
}
|
|
2706
|
+
async function findTemplate(options) {
|
|
2707
|
+
const { entityRef, token, catalogApi } = options;
|
|
2708
|
+
if (entityRef.kind.toLocaleLowerCase("en-US") !== "template") {
|
|
2709
|
+
throw new errors.InputError(`Invalid kind, only 'Template' kind is supported`);
|
|
2710
|
+
}
|
|
2711
|
+
const template = await catalogApi.getEntityByRef(entityRef, { token });
|
|
2712
|
+
if (!template) {
|
|
2713
|
+
throw new errors.NotFoundError(
|
|
2714
|
+
`Template ${catalogModel.stringifyEntityRef(entityRef)} not found`
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
return template;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
function isTemplatePermissionRuleInput(permissionRule) {
|
|
2721
|
+
return permissionRule.resourceType === alpha.RESOURCE_TYPE_SCAFFOLDER_TEMPLATE;
|
|
2722
|
+
}
|
|
2723
|
+
function isActionPermissionRuleInput(permissionRule) {
|
|
2724
|
+
return permissionRule.resourceType === alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION;
|
|
2725
|
+
}
|
|
2726
|
+
function isSupportedTemplate(entity) {
|
|
2727
|
+
return entity.apiVersion === "scaffolder.backstage.io/v1beta3";
|
|
2728
|
+
}
|
|
2729
|
+
function buildDefaultIdentityClient(options) {
|
|
2730
|
+
return {
|
|
2731
|
+
getIdentity: async ({ request }) => {
|
|
2732
|
+
var _a;
|
|
2733
|
+
const header = request.headers.authorization;
|
|
2734
|
+
const { logger } = options;
|
|
2735
|
+
if (!header) {
|
|
2736
|
+
return void 0;
|
|
2737
|
+
}
|
|
2738
|
+
try {
|
|
2739
|
+
const token = (_a = header.match(/^Bearer\s(\S+\.\S+\.\S+)$/i)) == null ? void 0 : _a[1];
|
|
2740
|
+
if (!token) {
|
|
2741
|
+
throw new TypeError("Expected Bearer with JWT");
|
|
2742
|
+
}
|
|
2743
|
+
const [_header, rawPayload, _signature] = token.split(".");
|
|
2744
|
+
const payload = JSON.parse(
|
|
2745
|
+
Buffer.from(rawPayload, "base64").toString()
|
|
2746
|
+
);
|
|
2747
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
2748
|
+
throw new TypeError("Malformed JWT payload");
|
|
2749
|
+
}
|
|
2750
|
+
const sub = payload.sub;
|
|
2751
|
+
if (typeof sub !== "string") {
|
|
2752
|
+
throw new TypeError("Expected string sub claim");
|
|
2753
|
+
}
|
|
2754
|
+
if (sub === "backstage-server") {
|
|
2755
|
+
return void 0;
|
|
2756
|
+
}
|
|
2757
|
+
catalogModel.parseEntityRef(sub);
|
|
2758
|
+
return {
|
|
2759
|
+
identity: {
|
|
2760
|
+
userEntityRef: sub,
|
|
2761
|
+
ownershipEntityRefs: [],
|
|
2762
|
+
type: "user"
|
|
2763
|
+
},
|
|
2764
|
+
token
|
|
2765
|
+
};
|
|
2766
|
+
} catch (e) {
|
|
2767
|
+
logger.error(`Invalid authorization header: ${errors.stringifyError(e)}`);
|
|
2768
|
+
return void 0;
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
const readDuration = (config$1, key, defaultValue) => {
|
|
2774
|
+
if (config$1.has(key)) {
|
|
2775
|
+
return config.readDurationFromConfig(config$1, { key });
|
|
2776
|
+
}
|
|
2777
|
+
return defaultValue;
|
|
2778
|
+
};
|
|
2779
|
+
async function createRouter(options) {
|
|
2780
|
+
var _a;
|
|
2781
|
+
const router = Router__default["default"]();
|
|
2782
|
+
router.use(express__default["default"].json({ limit: "10MB" }));
|
|
2783
|
+
const {
|
|
2784
|
+
logger: parentLogger,
|
|
2785
|
+
config,
|
|
2786
|
+
reader,
|
|
2787
|
+
database,
|
|
2788
|
+
catalogClient,
|
|
2789
|
+
actions,
|
|
2790
|
+
taskWorkers,
|
|
2791
|
+
scheduler,
|
|
2792
|
+
additionalTemplateFilters,
|
|
2793
|
+
additionalTemplateGlobals,
|
|
2794
|
+
permissions,
|
|
2795
|
+
permissionRules
|
|
2796
|
+
} = options;
|
|
2797
|
+
const concurrentTasksLimit = (_a = options.concurrentTasksLimit) != null ? _a : options.config.getOptionalNumber("scaffolder.concurrentTasksLimit");
|
|
2798
|
+
const logger = parentLogger.child({ plugin: "scaffolder" });
|
|
2799
|
+
const identity = options.identity || buildDefaultIdentityClient(options);
|
|
2800
|
+
const workingDirectory = await getWorkingDirectory(config, logger);
|
|
2801
|
+
const integrations = integration.ScmIntegrations.fromConfig(config);
|
|
2802
|
+
let taskBroker;
|
|
2803
|
+
if (!options.taskBroker) {
|
|
2804
|
+
const databaseTaskStore = await DatabaseTaskStore.create({ database });
|
|
2805
|
+
taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
|
|
2806
|
+
if (scheduler && databaseTaskStore.listStaleTasks) {
|
|
2807
|
+
await scheduler.scheduleTask({
|
|
2808
|
+
id: "close_stale_tasks",
|
|
2809
|
+
frequency: readDuration(
|
|
2810
|
+
config,
|
|
2811
|
+
"scaffolder.taskTimeoutJanitorFrequency",
|
|
2812
|
+
{
|
|
2813
|
+
minutes: 5
|
|
2814
|
+
}
|
|
2815
|
+
),
|
|
2816
|
+
timeout: { minutes: 15 },
|
|
2817
|
+
fn: async () => {
|
|
2818
|
+
const { tasks } = await databaseTaskStore.listStaleTasks({
|
|
2819
|
+
timeoutS: luxon.Duration.fromObject(
|
|
2820
|
+
readDuration(config, "scaffolder.taskTimeout", {
|
|
2821
|
+
hours: 24
|
|
2822
|
+
})
|
|
2823
|
+
).as("seconds")
|
|
2824
|
+
});
|
|
2825
|
+
for (const task of tasks) {
|
|
2826
|
+
await databaseTaskStore.shutdownTask(task);
|
|
2827
|
+
logger.info(`Successfully closed stale task ${task.taskId}`);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
} else {
|
|
2833
|
+
taskBroker = options.taskBroker;
|
|
2834
|
+
}
|
|
2835
|
+
const actionRegistry = new TemplateActionRegistry();
|
|
2836
|
+
const workers = [];
|
|
2837
|
+
if (concurrentTasksLimit !== 0) {
|
|
2838
|
+
for (let i = 0; i < (taskWorkers || 1); i++) {
|
|
2839
|
+
const worker = await TaskWorker.create({
|
|
2840
|
+
taskBroker,
|
|
2841
|
+
actionRegistry,
|
|
2842
|
+
integrations,
|
|
2843
|
+
logger,
|
|
2844
|
+
workingDirectory,
|
|
2845
|
+
additionalTemplateFilters,
|
|
2846
|
+
additionalTemplateGlobals,
|
|
2847
|
+
concurrentTasksLimit,
|
|
2848
|
+
permissions
|
|
2849
|
+
});
|
|
2850
|
+
workers.push(worker);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
const actionsToRegister = Array.isArray(actions) ? actions : createBuiltinActions({
|
|
2854
|
+
integrations,
|
|
2855
|
+
catalogClient,
|
|
2856
|
+
reader,
|
|
2857
|
+
config,
|
|
2858
|
+
additionalTemplateFilters,
|
|
2859
|
+
additionalTemplateGlobals
|
|
2860
|
+
});
|
|
2861
|
+
actionsToRegister.forEach((action) => actionRegistry.register(action));
|
|
2862
|
+
workers.forEach((worker) => worker.start());
|
|
2863
|
+
const dryRunner = createDryRunner({
|
|
2864
|
+
actionRegistry,
|
|
2865
|
+
integrations,
|
|
2866
|
+
logger,
|
|
2867
|
+
workingDirectory,
|
|
2868
|
+
additionalTemplateFilters,
|
|
2869
|
+
additionalTemplateGlobals,
|
|
2870
|
+
permissions
|
|
2871
|
+
});
|
|
2872
|
+
const templateRules = Object.values(
|
|
2873
|
+
scaffolderTemplateRules
|
|
2874
|
+
);
|
|
2875
|
+
const actionRules = Object.values(
|
|
2876
|
+
scaffolderActionRules
|
|
2877
|
+
);
|
|
2878
|
+
if (permissionRules) {
|
|
2879
|
+
templateRules.push(
|
|
2880
|
+
...permissionRules.filter(isTemplatePermissionRuleInput)
|
|
2881
|
+
);
|
|
2882
|
+
actionRules.push(...permissionRules.filter(isActionPermissionRuleInput));
|
|
2883
|
+
}
|
|
2884
|
+
const isAuthorized = pluginPermissionNode.createConditionAuthorizer(Object.values(templateRules));
|
|
2885
|
+
const permissionIntegrationRouter = pluginPermissionNode.createPermissionIntegrationRouter({
|
|
2886
|
+
resources: [
|
|
2887
|
+
{
|
|
2888
|
+
resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
|
2889
|
+
permissions: alpha.scaffolderTemplatePermissions,
|
|
2890
|
+
rules: templateRules
|
|
2891
|
+
},
|
|
2892
|
+
{
|
|
2893
|
+
resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION,
|
|
2894
|
+
permissions: alpha.scaffolderActionPermissions,
|
|
2895
|
+
rules: actionRules
|
|
2896
|
+
}
|
|
2897
|
+
]
|
|
2898
|
+
});
|
|
2899
|
+
router.use(permissionIntegrationRouter);
|
|
2900
|
+
router.get(
|
|
2901
|
+
"/v2/templates/:namespace/:kind/:name/parameter-schema",
|
|
2902
|
+
async (req, res) => {
|
|
2903
|
+
var _a2, _b;
|
|
2904
|
+
const userIdentity = await identity.getIdentity({
|
|
2905
|
+
request: req
|
|
2906
|
+
});
|
|
2907
|
+
const token = userIdentity == null ? void 0 : userIdentity.token;
|
|
2908
|
+
const template = await authorizeTemplate(req.params, token);
|
|
2909
|
+
const parameters = [(_a2 = template.spec.parameters) != null ? _a2 : []].flat();
|
|
2910
|
+
const presentation = template.spec.presentation;
|
|
2911
|
+
res.json({
|
|
2912
|
+
title: (_b = template.metadata.title) != null ? _b : template.metadata.name,
|
|
2913
|
+
...presentation ? { presentation } : {},
|
|
2914
|
+
description: template.metadata.description,
|
|
2915
|
+
"ui:options": template.metadata["ui:options"],
|
|
2916
|
+
steps: parameters.map((schema) => {
|
|
2917
|
+
var _a3;
|
|
2918
|
+
return {
|
|
2919
|
+
title: (_a3 = schema.title) != null ? _a3 : "Please enter the following information",
|
|
2920
|
+
description: schema.description,
|
|
2921
|
+
schema
|
|
2922
|
+
};
|
|
2923
|
+
})
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
).get("/v2/actions", async (_req, res) => {
|
|
2927
|
+
const actionsList = actionRegistry.list().map((action) => {
|
|
2928
|
+
return {
|
|
2929
|
+
id: action.id,
|
|
2930
|
+
description: action.description,
|
|
2931
|
+
examples: action.examples,
|
|
2932
|
+
schema: action.schema
|
|
2933
|
+
};
|
|
2934
|
+
});
|
|
2935
|
+
res.json(actionsList);
|
|
2936
|
+
}).post("/v2/tasks", async (req, res) => {
|
|
2937
|
+
var _a2, _b;
|
|
2938
|
+
const templateRef = req.body.templateRef;
|
|
2939
|
+
const { kind, namespace, name } = catalogModel.parseEntityRef(templateRef, {
|
|
2940
|
+
defaultKind: "template"
|
|
2941
|
+
});
|
|
2942
|
+
const callerIdentity = await identity.getIdentity({
|
|
2943
|
+
request: req
|
|
2944
|
+
});
|
|
2945
|
+
const token = callerIdentity == null ? void 0 : callerIdentity.token;
|
|
2946
|
+
const userEntityRef = callerIdentity == null ? void 0 : callerIdentity.identity.userEntityRef;
|
|
2947
|
+
const userEntity = userEntityRef ? await catalogClient.getEntityByRef(userEntityRef, { token }) : void 0;
|
|
2948
|
+
let auditLog = `Scaffolding task for ${templateRef}`;
|
|
2949
|
+
if (userEntityRef) {
|
|
2950
|
+
auditLog += ` created by ${userEntityRef}`;
|
|
2951
|
+
}
|
|
2952
|
+
logger.info(auditLog);
|
|
2953
|
+
const values = req.body.values;
|
|
2954
|
+
const template = await authorizeTemplate(
|
|
2955
|
+
{ kind, namespace, name },
|
|
2956
|
+
token
|
|
2957
|
+
);
|
|
2958
|
+
for (const parameters of [(_a2 = template.spec.parameters) != null ? _a2 : []].flat()) {
|
|
2959
|
+
const result2 = jsonschema.validate(values, parameters);
|
|
2960
|
+
if (!result2.valid) {
|
|
2961
|
+
res.status(400).json({ errors: result2.errors });
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
const baseUrl = getEntityBaseUrl(template);
|
|
2966
|
+
const taskSpec = {
|
|
2967
|
+
apiVersion: template.apiVersion,
|
|
2968
|
+
steps: template.spec.steps.map((step, index) => {
|
|
2969
|
+
var _a3, _b2;
|
|
2970
|
+
return {
|
|
2971
|
+
...step,
|
|
2972
|
+
id: (_a3 = step.id) != null ? _a3 : `step-${index + 1}`,
|
|
2973
|
+
name: (_b2 = step.name) != null ? _b2 : step.action
|
|
2974
|
+
};
|
|
2975
|
+
}),
|
|
2976
|
+
output: (_b = template.spec.output) != null ? _b : {},
|
|
2977
|
+
parameters: values,
|
|
2978
|
+
user: {
|
|
2979
|
+
entity: userEntity,
|
|
2980
|
+
ref: userEntityRef
|
|
2981
|
+
},
|
|
2982
|
+
templateInfo: {
|
|
2983
|
+
entityRef: catalogModel.stringifyEntityRef({ kind, name, namespace }),
|
|
2984
|
+
baseUrl,
|
|
2985
|
+
entity: {
|
|
2986
|
+
metadata: template.metadata
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
};
|
|
2990
|
+
const result = await taskBroker.dispatch({
|
|
2991
|
+
spec: taskSpec,
|
|
2992
|
+
createdBy: userEntityRef,
|
|
2993
|
+
secrets: {
|
|
2994
|
+
...req.body.secrets,
|
|
2995
|
+
backstageToken: token
|
|
2996
|
+
}
|
|
2997
|
+
});
|
|
2998
|
+
res.status(201).json({ id: result.taskId });
|
|
2999
|
+
}).get("/v2/tasks", async (req, res) => {
|
|
3000
|
+
const [userEntityRef] = [req.query.createdBy].flat();
|
|
3001
|
+
if (typeof userEntityRef !== "string" && typeof userEntityRef !== "undefined") {
|
|
3002
|
+
throw new errors.InputError("createdBy query parameter must be a string");
|
|
3003
|
+
}
|
|
3004
|
+
if (!taskBroker.list) {
|
|
3005
|
+
throw new Error(
|
|
3006
|
+
"TaskBroker does not support listing tasks, please implement the list method on the TaskBroker."
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
const tasks = await taskBroker.list({
|
|
3010
|
+
createdBy: userEntityRef
|
|
3011
|
+
});
|
|
3012
|
+
res.status(200).json(tasks);
|
|
3013
|
+
}).get("/v2/tasks/:taskId", async (req, res) => {
|
|
3014
|
+
const { taskId } = req.params;
|
|
3015
|
+
const task = await taskBroker.get(taskId);
|
|
3016
|
+
if (!task) {
|
|
3017
|
+
throw new errors.NotFoundError(`Task with id ${taskId} does not exist`);
|
|
3018
|
+
}
|
|
3019
|
+
delete task.secrets;
|
|
3020
|
+
res.status(200).json(task);
|
|
3021
|
+
}).post("/v2/tasks/:taskId/cancel", async (req, res) => {
|
|
3022
|
+
var _a2;
|
|
3023
|
+
const { taskId } = req.params;
|
|
3024
|
+
await ((_a2 = taskBroker.cancel) == null ? void 0 : _a2.call(taskBroker, taskId));
|
|
3025
|
+
res.status(200).json({ status: "cancelled" });
|
|
3026
|
+
}).get("/v2/tasks/:taskId/eventstream", async (req, res) => {
|
|
3027
|
+
const { taskId } = req.params;
|
|
3028
|
+
const after = req.query.after !== void 0 ? Number(req.query.after) : void 0;
|
|
3029
|
+
logger.debug(`Event stream observing taskId '${taskId}' opened`);
|
|
3030
|
+
res.writeHead(200, {
|
|
3031
|
+
Connection: "keep-alive",
|
|
3032
|
+
"Cache-Control": "no-cache",
|
|
3033
|
+
"Content-Type": "text/event-stream"
|
|
3034
|
+
});
|
|
3035
|
+
const subscription = taskBroker.event$({ taskId, after }).subscribe({
|
|
3036
|
+
error: (error) => {
|
|
3037
|
+
logger.error(
|
|
3038
|
+
`Received error from event stream when observing taskId '${taskId}', ${error}`
|
|
3039
|
+
);
|
|
3040
|
+
res.end();
|
|
3041
|
+
},
|
|
3042
|
+
next: ({ events }) => {
|
|
3043
|
+
var _a2;
|
|
3044
|
+
let shouldUnsubscribe = false;
|
|
3045
|
+
for (const event of events) {
|
|
3046
|
+
res.write(
|
|
3047
|
+
`event: ${event.type}
|
|
3048
|
+
data: ${JSON.stringify(event)}
|
|
3049
|
+
|
|
3050
|
+
`
|
|
3051
|
+
);
|
|
3052
|
+
if (event.type === "completion") {
|
|
3053
|
+
shouldUnsubscribe = true;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
(_a2 = res.flush) == null ? void 0 : _a2.call(res);
|
|
3057
|
+
if (shouldUnsubscribe) {
|
|
3058
|
+
subscription.unsubscribe();
|
|
3059
|
+
res.end();
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
});
|
|
3063
|
+
req.on("close", () => {
|
|
3064
|
+
subscription.unsubscribe();
|
|
3065
|
+
logger.debug(`Event stream observing taskId '${taskId}' closed`);
|
|
3066
|
+
});
|
|
3067
|
+
}).get("/v2/tasks/:taskId/events", async (req, res) => {
|
|
3068
|
+
const { taskId } = req.params;
|
|
3069
|
+
const after = Number(req.query.after) || void 0;
|
|
3070
|
+
const timeout = setTimeout(() => {
|
|
3071
|
+
res.json([]);
|
|
3072
|
+
}, 3e4);
|
|
3073
|
+
const subscription = taskBroker.event$({ taskId, after }).subscribe({
|
|
3074
|
+
error: (error) => {
|
|
3075
|
+
logger.error(
|
|
3076
|
+
`Received error from event stream when observing taskId '${taskId}', ${error}`
|
|
3077
|
+
);
|
|
3078
|
+
},
|
|
3079
|
+
next: ({ events }) => {
|
|
3080
|
+
clearTimeout(timeout);
|
|
3081
|
+
subscription.unsubscribe();
|
|
3082
|
+
res.json(events);
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
req.on("close", () => {
|
|
3086
|
+
subscription.unsubscribe();
|
|
3087
|
+
clearTimeout(timeout);
|
|
3088
|
+
});
|
|
3089
|
+
}).post("/v2/dry-run", async (req, res) => {
|
|
3090
|
+
var _a2, _b, _c, _d;
|
|
3091
|
+
const bodySchema = zod.z.object({
|
|
3092
|
+
template: zod.z.unknown(),
|
|
3093
|
+
values: zod.z.record(zod.z.unknown()),
|
|
3094
|
+
secrets: zod.z.record(zod.z.string()).optional(),
|
|
3095
|
+
directoryContents: zod.z.array(
|
|
3096
|
+
zod.z.object({ path: zod.z.string(), base64Content: zod.z.string() })
|
|
3097
|
+
)
|
|
3098
|
+
});
|
|
3099
|
+
const body = await bodySchema.parseAsync(req.body).catch((e) => {
|
|
3100
|
+
throw new errors.InputError(`Malformed request: ${e}`);
|
|
3101
|
+
});
|
|
3102
|
+
const template = body.template;
|
|
3103
|
+
if (!await pluginScaffolderCommon.templateEntityV1beta3Validator.check(template)) {
|
|
3104
|
+
throw new errors.InputError("Input template is not a template");
|
|
3105
|
+
}
|
|
3106
|
+
const token = (_a2 = await identity.getIdentity({
|
|
3107
|
+
request: req
|
|
3108
|
+
})) == null ? void 0 : _a2.token;
|
|
3109
|
+
for (const parameters of [(_b = template.spec.parameters) != null ? _b : []].flat()) {
|
|
3110
|
+
const result2 = jsonschema.validate(body.values, parameters);
|
|
3111
|
+
if (!result2.valid) {
|
|
3112
|
+
res.status(400).json({ errors: result2.errors });
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
const steps = template.spec.steps.map((step, index) => {
|
|
3117
|
+
var _a3, _b2;
|
|
3118
|
+
return {
|
|
3119
|
+
...step,
|
|
3120
|
+
id: (_a3 = step.id) != null ? _a3 : `step-${index + 1}`,
|
|
3121
|
+
name: (_b2 = step.name) != null ? _b2 : step.action
|
|
3122
|
+
};
|
|
3123
|
+
});
|
|
3124
|
+
const result = await dryRunner({
|
|
3125
|
+
spec: {
|
|
3126
|
+
apiVersion: template.apiVersion,
|
|
3127
|
+
steps,
|
|
3128
|
+
output: (_c = template.spec.output) != null ? _c : {},
|
|
3129
|
+
parameters: body.values
|
|
3130
|
+
},
|
|
3131
|
+
directoryContents: ((_d = body.directoryContents) != null ? _d : []).map((file) => ({
|
|
3132
|
+
path: file.path,
|
|
3133
|
+
content: Buffer.from(file.base64Content, "base64")
|
|
3134
|
+
})),
|
|
3135
|
+
secrets: {
|
|
3136
|
+
...body.secrets,
|
|
3137
|
+
...token && { backstageToken: token }
|
|
3138
|
+
}
|
|
3139
|
+
});
|
|
3140
|
+
res.status(200).json({
|
|
3141
|
+
...result,
|
|
3142
|
+
steps,
|
|
3143
|
+
directoryContents: result.directoryContents.map((file) => ({
|
|
3144
|
+
path: file.path,
|
|
3145
|
+
executable: file.executable,
|
|
3146
|
+
base64Content: file.content.toString("base64")
|
|
3147
|
+
}))
|
|
3148
|
+
});
|
|
3149
|
+
});
|
|
3150
|
+
const app = express__default["default"]();
|
|
3151
|
+
app.set("logger", logger);
|
|
3152
|
+
app.use("/", router);
|
|
3153
|
+
async function authorizeTemplate(entityRef, token) {
|
|
3154
|
+
const template = await findTemplate({
|
|
3155
|
+
catalogApi: catalogClient,
|
|
3156
|
+
entityRef,
|
|
3157
|
+
token
|
|
3158
|
+
});
|
|
3159
|
+
if (!isSupportedTemplate(template)) {
|
|
3160
|
+
throw new errors.InputError(
|
|
3161
|
+
`Unsupported apiVersion field in schema entity, ${template.apiVersion}`
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
if (!permissions) {
|
|
3165
|
+
return template;
|
|
3166
|
+
}
|
|
3167
|
+
const [parameterDecision, stepDecision] = await permissions.authorizeConditional(
|
|
3168
|
+
[
|
|
3169
|
+
{ permission: alpha.templateParameterReadPermission },
|
|
3170
|
+
{ permission: alpha.templateStepReadPermission }
|
|
3171
|
+
],
|
|
3172
|
+
{ token }
|
|
3173
|
+
);
|
|
3174
|
+
if (Array.isArray(template.spec.parameters)) {
|
|
3175
|
+
template.spec.parameters = template.spec.parameters.filter(
|
|
3176
|
+
(step) => isAuthorized(parameterDecision, step)
|
|
3177
|
+
);
|
|
3178
|
+
} else if (template.spec.parameters && !isAuthorized(parameterDecision, template.spec.parameters)) {
|
|
3179
|
+
template.spec.parameters = void 0;
|
|
3180
|
+
}
|
|
3181
|
+
template.spec.steps = template.spec.steps.filter(
|
|
3182
|
+
(step) => isAuthorized(stepDecision, step)
|
|
3183
|
+
);
|
|
3184
|
+
return template;
|
|
3185
|
+
}
|
|
3186
|
+
return app;
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
exports.DatabaseTaskStore = DatabaseTaskStore;
|
|
3190
|
+
exports.TaskManager = TaskManager;
|
|
3191
|
+
exports.TaskWorker = TaskWorker;
|
|
3192
|
+
exports.TemplateActionRegistry = TemplateActionRegistry;
|
|
3193
|
+
exports.createBuiltinActions = createBuiltinActions;
|
|
3194
|
+
exports.createCatalogRegisterAction = createCatalogRegisterAction;
|
|
3195
|
+
exports.createCatalogWriteAction = createCatalogWriteAction;
|
|
3196
|
+
exports.createDebugLogAction = createDebugLogAction;
|
|
3197
|
+
exports.createFetchCatalogEntityAction = createFetchCatalogEntityAction;
|
|
3198
|
+
exports.createFetchPlainAction = createFetchPlainAction;
|
|
3199
|
+
exports.createFetchPlainFileAction = createFetchPlainFileAction;
|
|
3200
|
+
exports.createFetchTemplateAction = createFetchTemplateAction;
|
|
3201
|
+
exports.createFilesystemDeleteAction = createFilesystemDeleteAction;
|
|
3202
|
+
exports.createFilesystemRenameAction = createFilesystemRenameAction;
|
|
3203
|
+
exports.createRouter = createRouter;
|
|
3204
|
+
exports.createWaitAction = createWaitAction;
|
|
3205
|
+
exports.scaffolderActionRules = scaffolderActionRules;
|
|
3206
|
+
exports.scaffolderTemplateRules = scaffolderTemplateRules;
|
|
3207
|
+
//# sourceMappingURL=router-46321f3f.cjs.js.map
|