@backstage/plugin-scaffolder-backend 1.29.0 → 1.30.0-next.0
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 +35 -0
- package/README.md +29 -0
- package/dist/ScaffolderPlugin.cjs.js +8 -5
- package/dist/ScaffolderPlugin.cjs.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/scaffolder/dryrun/createDryRunner.cjs.js +5 -4
- package/dist/scaffolder/dryrun/createDryRunner.cjs.js.map +1 -1
- package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js +18 -14
- package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js.map +1 -1
- package/dist/scaffolder/tasks/StorageTaskBroker.cjs.js +14 -5
- package/dist/scaffolder/tasks/StorageTaskBroker.cjs.js.map +1 -1
- package/dist/scaffolder/tasks/TaskWorker.cjs.js +21 -2
- package/dist/scaffolder/tasks/TaskWorker.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +475 -309
- package/dist/service/router.cjs.js.map +1 -1
- package/package.json +31 -31
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var backendCommon = require('@backstage/backend-common');
|
|
4
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
5
|
var catalogModel = require('@backstage/catalog-model');
|
|
5
6
|
var config = require('@backstage/config');
|
|
6
7
|
var errors = require('@backstage/errors');
|
|
7
8
|
var integration = require('@backstage/integration');
|
|
9
|
+
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
8
10
|
var pluginScaffolderCommon = require('@backstage/plugin-scaffolder-common');
|
|
9
11
|
var alpha = require('@backstage/plugin-scaffolder-common/alpha');
|
|
10
12
|
var express = require('express');
|
|
11
13
|
var Router = require('express-promise-router');
|
|
12
14
|
var jsonschema = require('jsonschema');
|
|
15
|
+
var luxon = require('luxon');
|
|
16
|
+
var url = require('url');
|
|
17
|
+
var uuid = require('uuid');
|
|
13
18
|
var z = require('zod');
|
|
14
19
|
require('@backstage/plugin-scaffolder-node');
|
|
15
20
|
require('../scaffolder/actions/builtin/catalog/register.examples.cjs.js');
|
|
16
21
|
require('fs-extra');
|
|
17
22
|
require('yaml');
|
|
18
|
-
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
19
23
|
require('../scaffolder/actions/builtin/catalog/write.examples.cjs.js');
|
|
20
24
|
require('../scaffolder/actions/builtin/catalog/fetch.examples.cjs.js');
|
|
21
25
|
var createBuiltinActions = require('../scaffolder/actions/builtin/createBuiltinActions.cjs.js');
|
|
22
26
|
require('path');
|
|
23
27
|
require('../scaffolder/actions/builtin/debug/log.examples.cjs.js');
|
|
24
28
|
require('fs');
|
|
25
|
-
var luxon = require('luxon');
|
|
26
29
|
require('../scaffolder/actions/builtin/debug/wait.examples.cjs.js');
|
|
27
30
|
require('../scaffolder/actions/builtin/fetch/plain.examples.cjs.js');
|
|
28
31
|
require('../scaffolder/actions/builtin/fetch/plainFile.examples.cjs.js');
|
|
@@ -47,12 +50,9 @@ var DatabaseTaskStore = require('../scaffolder/tasks/DatabaseTaskStore.cjs.js');
|
|
|
47
50
|
var StorageTaskBroker = require('../scaffolder/tasks/StorageTaskBroker.cjs.js');
|
|
48
51
|
var TaskWorker = require('../scaffolder/tasks/TaskWorker.cjs.js');
|
|
49
52
|
var createDryRunner = require('../scaffolder/dryrun/createDryRunner.cjs.js');
|
|
53
|
+
var checkPermissions = require('../util/checkPermissions.cjs.js');
|
|
50
54
|
var helpers = require('./helpers.cjs.js');
|
|
51
|
-
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
52
55
|
var rules = require('./rules.cjs.js');
|
|
53
|
-
var checkPermissions = require('../util/checkPermissions.cjs.js');
|
|
54
|
-
var url = require('url');
|
|
55
|
-
var uuid = require('uuid');
|
|
56
56
|
|
|
57
57
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
58
58
|
|
|
@@ -137,7 +137,8 @@ async function createRouter(options) {
|
|
|
137
137
|
discovery = backendCommon.HostDiscovery.fromConfig(config),
|
|
138
138
|
identity = buildDefaultIdentityClient(options),
|
|
139
139
|
autocompleteHandlers = {},
|
|
140
|
-
events: eventsService
|
|
140
|
+
events: eventsService,
|
|
141
|
+
auditor
|
|
141
142
|
} = options;
|
|
142
143
|
const { auth, httpAuth } = backendCommon.createLegacyAuthAdapters({
|
|
143
144
|
...options,
|
|
@@ -159,7 +160,8 @@ async function createRouter(options) {
|
|
|
159
160
|
logger,
|
|
160
161
|
config,
|
|
161
162
|
auth,
|
|
162
|
-
additionalWorkspaceProviders
|
|
163
|
+
additionalWorkspaceProviders,
|
|
164
|
+
auditor
|
|
163
165
|
);
|
|
164
166
|
if (scheduler && databaseTaskStore.listStaleTasks) {
|
|
165
167
|
await scheduler.scheduleTask({
|
|
@@ -199,6 +201,7 @@ async function createRouter(options) {
|
|
|
199
201
|
actionRegistry,
|
|
200
202
|
integrations,
|
|
201
203
|
logger,
|
|
204
|
+
auditor,
|
|
202
205
|
workingDirectory,
|
|
203
206
|
additionalTemplateFilters,
|
|
204
207
|
additionalTemplateGlobals,
|
|
@@ -232,6 +235,7 @@ async function createRouter(options) {
|
|
|
232
235
|
actionRegistry,
|
|
233
236
|
integrations,
|
|
234
237
|
logger,
|
|
238
|
+
auditor,
|
|
235
239
|
workingDirectory,
|
|
236
240
|
additionalTemplateFilters,
|
|
237
241
|
additionalTemplateGlobals,
|
|
@@ -269,347 +273,509 @@ async function createRouter(options) {
|
|
|
269
273
|
router.get(
|
|
270
274
|
"/v2/templates/:namespace/:kind/:name/parameter-schema",
|
|
271
275
|
async (req, res) => {
|
|
276
|
+
const requestedTemplateRef = `${req.params.kind}:${req.params.namespace}/${req.params.name}`;
|
|
277
|
+
const auditorEvent = await auditor?.createEvent({
|
|
278
|
+
eventId: "template-parameter-schema",
|
|
279
|
+
request: req,
|
|
280
|
+
meta: { templateRef: requestedTemplateRef }
|
|
281
|
+
});
|
|
282
|
+
try {
|
|
283
|
+
const credentials = await httpAuth.credentials(req);
|
|
284
|
+
const { token } = await auth.getPluginRequestToken({
|
|
285
|
+
onBehalfOf: credentials,
|
|
286
|
+
targetPluginId: "catalog"
|
|
287
|
+
});
|
|
288
|
+
const template = await authorizeTemplate(
|
|
289
|
+
req.params,
|
|
290
|
+
token,
|
|
291
|
+
credentials
|
|
292
|
+
);
|
|
293
|
+
const parameters = [template.spec.parameters ?? []].flat();
|
|
294
|
+
const presentation = template.spec.presentation;
|
|
295
|
+
const templateRef = `${template.kind}:${template.metadata.namespace || "default"}/${template.metadata.name}`;
|
|
296
|
+
await auditorEvent?.success({ meta: { templateRef } });
|
|
297
|
+
res.json({
|
|
298
|
+
title: template.metadata.title ?? template.metadata.name,
|
|
299
|
+
...presentation ? { presentation } : {},
|
|
300
|
+
description: template.metadata.description,
|
|
301
|
+
"ui:options": template.metadata["ui:options"],
|
|
302
|
+
steps: parameters.map((schema) => ({
|
|
303
|
+
title: schema.title ?? "Please enter the following information",
|
|
304
|
+
description: schema.description,
|
|
305
|
+
schema
|
|
306
|
+
})),
|
|
307
|
+
EXPERIMENTAL_formDecorators: template.spec.EXPERIMENTAL_formDecorators
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
await auditorEvent?.fail({ error: err });
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
).get("/v2/actions", async (req, res) => {
|
|
315
|
+
const auditorEvent = await auditor?.createEvent({
|
|
316
|
+
eventId: "action-fetch",
|
|
317
|
+
request: req
|
|
318
|
+
});
|
|
319
|
+
try {
|
|
320
|
+
const actionsList = actionRegistry.list().map((action) => {
|
|
321
|
+
return {
|
|
322
|
+
id: action.id,
|
|
323
|
+
description: action.description,
|
|
324
|
+
examples: action.examples,
|
|
325
|
+
schema: action.schema
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
await auditorEvent?.success();
|
|
329
|
+
res.json(actionsList);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
await auditorEvent?.fail({ error: err });
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
}).post("/v2/tasks", async (req, res) => {
|
|
335
|
+
const templateRef = req.body.templateRef;
|
|
336
|
+
const { kind, namespace, name } = catalogModel.parseEntityRef(templateRef, {
|
|
337
|
+
defaultKind: "template"
|
|
338
|
+
});
|
|
339
|
+
const auditorEvent = await auditor?.createEvent({
|
|
340
|
+
eventId: "task",
|
|
341
|
+
severityLevel: "medium",
|
|
342
|
+
request: req,
|
|
343
|
+
meta: {
|
|
344
|
+
actionType: "create",
|
|
345
|
+
templateRef
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
try {
|
|
272
349
|
const credentials = await httpAuth.credentials(req);
|
|
350
|
+
await checkPermissions.checkPermission({
|
|
351
|
+
credentials,
|
|
352
|
+
permissions: [alpha.taskCreatePermission],
|
|
353
|
+
permissionService: permissions
|
|
354
|
+
});
|
|
273
355
|
const { token } = await auth.getPluginRequestToken({
|
|
274
356
|
onBehalfOf: credentials,
|
|
275
357
|
targetPluginId: "catalog"
|
|
276
358
|
});
|
|
359
|
+
const userEntityRef = auth.isPrincipal(credentials, "user") ? credentials.principal.userEntityRef : void 0;
|
|
360
|
+
const userEntity = userEntityRef ? await catalogClient.getEntityByRef(userEntityRef, { token }) : void 0;
|
|
361
|
+
let auditLog = `Scaffolding task for ${templateRef}`;
|
|
362
|
+
if (userEntityRef) {
|
|
363
|
+
auditLog += ` created by ${userEntityRef}`;
|
|
364
|
+
}
|
|
365
|
+
logger.info(auditLog);
|
|
366
|
+
const values = req.body.values;
|
|
277
367
|
const template = await authorizeTemplate(
|
|
278
|
-
|
|
368
|
+
{ kind, namespace, name },
|
|
279
369
|
token,
|
|
280
370
|
credentials
|
|
281
371
|
);
|
|
282
|
-
const parameters
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
372
|
+
for (const parameters of [template.spec.parameters ?? []].flat()) {
|
|
373
|
+
const result2 = jsonschema.validate(values, parameters);
|
|
374
|
+
if (!result2.valid) {
|
|
375
|
+
await auditorEvent?.fail({
|
|
376
|
+
// TODO(Rugvip): Seems like there aren't proper types for AggregateError yet
|
|
377
|
+
error: AggregateError(
|
|
378
|
+
result2.errors,
|
|
379
|
+
"Could not create entity"
|
|
380
|
+
)
|
|
381
|
+
});
|
|
382
|
+
res.status(400).json({ errors: result2.errors });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const baseUrl = helpers.getEntityBaseUrl(template);
|
|
387
|
+
const taskSpec = {
|
|
388
|
+
apiVersion: template.apiVersion,
|
|
389
|
+
steps: template.spec.steps.map((step, index) => ({
|
|
390
|
+
...step,
|
|
391
|
+
id: step.id ?? `step-${index + 1}`,
|
|
392
|
+
name: step.name ?? step.action
|
|
293
393
|
})),
|
|
294
|
-
|
|
394
|
+
EXPERIMENTAL_recovery: template.spec.EXPERIMENTAL_recovery,
|
|
395
|
+
output: template.spec.output ?? {},
|
|
396
|
+
parameters: values,
|
|
397
|
+
user: {
|
|
398
|
+
entity: userEntity,
|
|
399
|
+
ref: userEntityRef
|
|
400
|
+
},
|
|
401
|
+
templateInfo: {
|
|
402
|
+
entityRef: catalogModel.stringifyEntityRef({ kind, name, namespace }),
|
|
403
|
+
baseUrl,
|
|
404
|
+
entity: {
|
|
405
|
+
metadata: template.metadata
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const secrets = {
|
|
410
|
+
...req.body.secrets,
|
|
411
|
+
backstageToken: token,
|
|
412
|
+
__initiatorCredentials: JSON.stringify({
|
|
413
|
+
...credentials,
|
|
414
|
+
// credentials.token is nonenumerable and will not be serialized, so we need to add it explicitly
|
|
415
|
+
token: credentials.token
|
|
416
|
+
})
|
|
417
|
+
};
|
|
418
|
+
const result = await taskBroker.dispatch({
|
|
419
|
+
spec: taskSpec,
|
|
420
|
+
createdBy: userEntityRef,
|
|
421
|
+
secrets
|
|
295
422
|
});
|
|
423
|
+
await auditorEvent?.success({ meta: { taskId: result.taskId } });
|
|
424
|
+
res.status(201).json({ id: result.taskId });
|
|
425
|
+
} catch (err) {
|
|
426
|
+
await auditorEvent?.fail({ error: err });
|
|
427
|
+
throw err;
|
|
296
428
|
}
|
|
297
|
-
).get("/v2/
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
};
|
|
305
|
-
});
|
|
306
|
-
res.json(actionsList);
|
|
307
|
-
}).post("/v2/tasks", async (req, res) => {
|
|
308
|
-
const templateRef = req.body.templateRef;
|
|
309
|
-
const { kind, namespace, name } = catalogModel.parseEntityRef(templateRef, {
|
|
310
|
-
defaultKind: "template"
|
|
311
|
-
});
|
|
312
|
-
const credentials = await httpAuth.credentials(req);
|
|
313
|
-
await checkPermissions.checkPermission({
|
|
314
|
-
credentials,
|
|
315
|
-
permissions: [alpha.taskCreatePermission],
|
|
316
|
-
permissionService: permissions
|
|
317
|
-
});
|
|
318
|
-
const { token } = await auth.getPluginRequestToken({
|
|
319
|
-
onBehalfOf: credentials,
|
|
320
|
-
targetPluginId: "catalog"
|
|
429
|
+
}).get("/v2/tasks", async (req, res) => {
|
|
430
|
+
const auditorEvent = await auditor?.createEvent({
|
|
431
|
+
eventId: "task",
|
|
432
|
+
request: req,
|
|
433
|
+
meta: {
|
|
434
|
+
actionType: "list"
|
|
435
|
+
}
|
|
321
436
|
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
credentials
|
|
334
|
-
);
|
|
335
|
-
for (const parameters of [template.spec.parameters ?? []].flat()) {
|
|
336
|
-
const result2 = jsonschema.validate(values, parameters);
|
|
337
|
-
if (!result2.valid) {
|
|
338
|
-
res.status(400).json({ errors: result2.errors });
|
|
339
|
-
return;
|
|
437
|
+
try {
|
|
438
|
+
const credentials = await httpAuth.credentials(req);
|
|
439
|
+
await checkPermissions.checkPermission({
|
|
440
|
+
credentials,
|
|
441
|
+
permissions: [alpha.taskReadPermission],
|
|
442
|
+
permissionService: permissions
|
|
443
|
+
});
|
|
444
|
+
if (!taskBroker.list) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
"TaskBroker does not support listing tasks, please implement the list method on the TaskBroker."
|
|
447
|
+
);
|
|
340
448
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
})),
|
|
350
|
-
EXPERIMENTAL_recovery: template.spec.EXPERIMENTAL_recovery,
|
|
351
|
-
output: template.spec.output ?? {},
|
|
352
|
-
parameters: values,
|
|
353
|
-
user: {
|
|
354
|
-
entity: userEntity,
|
|
355
|
-
ref: userEntityRef
|
|
356
|
-
},
|
|
357
|
-
templateInfo: {
|
|
358
|
-
entityRef: catalogModel.stringifyEntityRef({ kind, name, namespace }),
|
|
359
|
-
baseUrl,
|
|
360
|
-
entity: {
|
|
361
|
-
metadata: template.metadata
|
|
449
|
+
const createdBy = helpers.parseStringsParam(req.query.createdBy, "createdBy");
|
|
450
|
+
const status = helpers.parseStringsParam(req.query.status, "status");
|
|
451
|
+
const order = helpers.parseStringsParam(req.query.order, "order")?.map((item) => {
|
|
452
|
+
const match = item.match(/^(asc|desc):(.+)$/);
|
|
453
|
+
if (!match) {
|
|
454
|
+
throw new errors.InputError(
|
|
455
|
+
`Invalid order parameter "${item}", expected "<asc or desc>:<field name>"`
|
|
456
|
+
);
|
|
362
457
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
});
|
|
387
|
-
if (!taskBroker.list) {
|
|
388
|
-
throw new Error(
|
|
389
|
-
"TaskBroker does not support listing tasks, please implement the list method on the TaskBroker."
|
|
390
|
-
);
|
|
458
|
+
return {
|
|
459
|
+
order: match[1],
|
|
460
|
+
field: match[2]
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
const limit = helpers.parseNumberParam(req.query.limit, "limit");
|
|
464
|
+
const offset = helpers.parseNumberParam(req.query.offset, "offset");
|
|
465
|
+
const tasks = await taskBroker.list({
|
|
466
|
+
filters: {
|
|
467
|
+
createdBy,
|
|
468
|
+
status: status ? status : void 0
|
|
469
|
+
},
|
|
470
|
+
order,
|
|
471
|
+
pagination: {
|
|
472
|
+
limit: limit ? limit[0] : void 0,
|
|
473
|
+
offset: offset ? offset[0] : void 0
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
await auditorEvent?.success();
|
|
477
|
+
res.status(200).json(tasks);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
await auditorEvent?.fail({ error: err });
|
|
480
|
+
throw err;
|
|
391
481
|
}
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
482
|
+
}).get("/v2/tasks/:taskId", async (req, res) => {
|
|
483
|
+
const { taskId } = req.params;
|
|
484
|
+
const auditorEvent = await auditor?.createEvent({
|
|
485
|
+
eventId: "task",
|
|
486
|
+
request: req,
|
|
487
|
+
meta: {
|
|
488
|
+
actionType: "get",
|
|
489
|
+
taskId
|
|
400
490
|
}
|
|
401
|
-
return {
|
|
402
|
-
order: match[1],
|
|
403
|
-
field: match[2]
|
|
404
|
-
};
|
|
405
491
|
});
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
offset: offset ? offset[0] : void 0
|
|
492
|
+
try {
|
|
493
|
+
const credentials = await httpAuth.credentials(req);
|
|
494
|
+
await checkPermissions.checkPermission({
|
|
495
|
+
credentials,
|
|
496
|
+
permissions: [alpha.taskReadPermission],
|
|
497
|
+
permissionService: permissions
|
|
498
|
+
});
|
|
499
|
+
const task = await taskBroker.get(taskId);
|
|
500
|
+
if (!task) {
|
|
501
|
+
throw new errors.NotFoundError(`Task with id ${taskId} does not exist`);
|
|
417
502
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
permissions: [alpha.taskReadPermission],
|
|
425
|
-
permissionService: permissions
|
|
426
|
-
});
|
|
427
|
-
const { taskId } = req.params;
|
|
428
|
-
const task = await taskBroker.get(taskId);
|
|
429
|
-
if (!task) {
|
|
430
|
-
throw new errors.NotFoundError(`Task with id ${taskId} does not exist`);
|
|
503
|
+
await auditorEvent?.success();
|
|
504
|
+
delete task.secrets;
|
|
505
|
+
res.status(200).json(task);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
await auditorEvent?.fail({ error: err });
|
|
508
|
+
throw err;
|
|
431
509
|
}
|
|
432
|
-
delete task.secrets;
|
|
433
|
-
res.status(200).json(task);
|
|
434
510
|
}).post("/v2/tasks/:taskId/cancel", async (req, res) => {
|
|
435
|
-
const credentials = await httpAuth.credentials(req);
|
|
436
|
-
await checkPermissions.checkPermission({
|
|
437
|
-
credentials,
|
|
438
|
-
permissions: [alpha.taskCancelPermission, alpha.taskReadPermission],
|
|
439
|
-
permissionService: permissions
|
|
440
|
-
});
|
|
441
511
|
const { taskId } = req.params;
|
|
442
|
-
await
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
512
|
+
const auditorEvent = await auditor?.createEvent({
|
|
513
|
+
eventId: "task",
|
|
514
|
+
severityLevel: "medium",
|
|
515
|
+
request: req,
|
|
516
|
+
meta: {
|
|
517
|
+
actionType: "cancel",
|
|
518
|
+
taskId
|
|
519
|
+
}
|
|
450
520
|
});
|
|
521
|
+
try {
|
|
522
|
+
const credentials = await httpAuth.credentials(req);
|
|
523
|
+
await checkPermissions.checkPermission({
|
|
524
|
+
credentials,
|
|
525
|
+
permissions: [alpha.taskCancelPermission, alpha.taskReadPermission],
|
|
526
|
+
permissionService: permissions
|
|
527
|
+
});
|
|
528
|
+
await taskBroker.cancel?.(taskId);
|
|
529
|
+
await auditorEvent?.success();
|
|
530
|
+
res.status(200).json({ status: "cancelled" });
|
|
531
|
+
} catch (err) {
|
|
532
|
+
await auditorEvent?.fail({ error: err });
|
|
533
|
+
throw err;
|
|
534
|
+
}
|
|
535
|
+
}).post("/v2/tasks/:taskId/retry", async (req, res) => {
|
|
451
536
|
const { taskId } = req.params;
|
|
452
|
-
await
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
537
|
+
const auditorEvent = await auditor?.createEvent({
|
|
538
|
+
eventId: "task",
|
|
539
|
+
severityLevel: "medium",
|
|
540
|
+
request: req,
|
|
541
|
+
meta: {
|
|
542
|
+
actionType: "retry",
|
|
543
|
+
taskId
|
|
544
|
+
}
|
|
460
545
|
});
|
|
546
|
+
try {
|
|
547
|
+
const credentials = await httpAuth.credentials(req);
|
|
548
|
+
await checkPermissions.checkPermission({
|
|
549
|
+
credentials,
|
|
550
|
+
permissions: [alpha.taskCreatePermission, alpha.taskReadPermission],
|
|
551
|
+
permissionService: permissions
|
|
552
|
+
});
|
|
553
|
+
await auditorEvent?.success();
|
|
554
|
+
await taskBroker.retry?.(taskId);
|
|
555
|
+
res.status(201).json({ id: taskId });
|
|
556
|
+
} catch (err) {
|
|
557
|
+
await auditorEvent?.fail({ error: err });
|
|
558
|
+
throw err;
|
|
559
|
+
}
|
|
560
|
+
}).get("/v2/tasks/:taskId/eventstream", async (req, res) => {
|
|
461
561
|
const { taskId } = req.params;
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
562
|
+
const auditorEvent = await auditor?.createEvent({
|
|
563
|
+
eventId: "task",
|
|
564
|
+
request: req,
|
|
565
|
+
meta: {
|
|
566
|
+
actionType: "stream",
|
|
567
|
+
taskId
|
|
568
|
+
}
|
|
468
569
|
});
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
570
|
+
try {
|
|
571
|
+
const credentials = await httpAuth.credentials(req);
|
|
572
|
+
await checkPermissions.checkPermission({
|
|
573
|
+
credentials,
|
|
574
|
+
permissions: [alpha.taskReadPermission],
|
|
575
|
+
permissionService: permissions
|
|
576
|
+
});
|
|
577
|
+
const after = req.query.after !== void 0 ? Number(req.query.after) : void 0;
|
|
578
|
+
logger.debug(`Event stream observing taskId '${taskId}' opened`);
|
|
579
|
+
res.writeHead(200, {
|
|
580
|
+
Connection: "keep-alive",
|
|
581
|
+
"Cache-Control": "no-cache",
|
|
582
|
+
"Content-Type": "text/event-stream"
|
|
583
|
+
});
|
|
584
|
+
const subscription = taskBroker.event$({ taskId, after }).subscribe({
|
|
585
|
+
error: async (error) => {
|
|
586
|
+
logger.error(
|
|
587
|
+
`Received error from event stream when observing taskId '${taskId}', ${error}`
|
|
588
|
+
);
|
|
589
|
+
await auditorEvent?.fail({ error });
|
|
590
|
+
res.end();
|
|
591
|
+
},
|
|
592
|
+
next: ({ events }) => {
|
|
593
|
+
let shouldUnsubscribe = false;
|
|
594
|
+
for (const event of events) {
|
|
595
|
+
res.write(
|
|
596
|
+
`event: ${event.type}
|
|
481
597
|
data: ${JSON.stringify(event)}
|
|
482
598
|
|
|
483
599
|
`
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
600
|
+
);
|
|
601
|
+
if (event.type === "completion" && !event.isTaskRecoverable) {
|
|
602
|
+
shouldUnsubscribe = true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
res.flush?.();
|
|
606
|
+
if (shouldUnsubscribe) {
|
|
607
|
+
subscription.unsubscribe();
|
|
608
|
+
res.end();
|
|
487
609
|
}
|
|
488
610
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
});
|
|
611
|
+
});
|
|
612
|
+
req.on("close", async () => {
|
|
613
|
+
subscription.unsubscribe();
|
|
614
|
+
logger.debug(`Event stream observing taskId '${taskId}' closed`);
|
|
615
|
+
await auditorEvent?.success();
|
|
616
|
+
});
|
|
617
|
+
} catch (err) {
|
|
618
|
+
await auditorEvent?.fail({ error: err });
|
|
619
|
+
throw err;
|
|
620
|
+
}
|
|
500
621
|
}).get("/v2/tasks/:taskId/events", async (req, res) => {
|
|
501
|
-
const credentials = await httpAuth.credentials(req);
|
|
502
|
-
await checkPermissions.checkPermission({
|
|
503
|
-
credentials,
|
|
504
|
-
permissions: [alpha.taskReadPermission],
|
|
505
|
-
permissionService: permissions
|
|
506
|
-
});
|
|
507
622
|
const { taskId } = req.params;
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
logger.error(
|
|
515
|
-
`Received error from event stream when observing taskId '${taskId}', ${error}`
|
|
516
|
-
);
|
|
517
|
-
},
|
|
518
|
-
next: ({ events }) => {
|
|
519
|
-
clearTimeout(timeout);
|
|
520
|
-
subscription.unsubscribe();
|
|
521
|
-
res.json(events);
|
|
623
|
+
const auditorEvent = await auditor?.createEvent({
|
|
624
|
+
eventId: "task",
|
|
625
|
+
request: req,
|
|
626
|
+
meta: {
|
|
627
|
+
actionType: "events",
|
|
628
|
+
taskId
|
|
522
629
|
}
|
|
523
630
|
});
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
631
|
+
try {
|
|
632
|
+
const credentials = await httpAuth.credentials(req);
|
|
633
|
+
await checkPermissions.checkPermission({
|
|
634
|
+
credentials,
|
|
635
|
+
permissions: [alpha.taskReadPermission],
|
|
636
|
+
permissionService: permissions
|
|
637
|
+
});
|
|
638
|
+
const after = Number(req.query.after) || void 0;
|
|
639
|
+
const timeout = setTimeout(() => {
|
|
640
|
+
res.json([]);
|
|
641
|
+
}, 3e4);
|
|
642
|
+
const subscription = taskBroker.event$({ taskId, after }).subscribe({
|
|
643
|
+
error: async (error) => {
|
|
644
|
+
logger.error(
|
|
645
|
+
`Received error from event stream when observing taskId '${taskId}', ${error}`
|
|
646
|
+
);
|
|
647
|
+
await auditorEvent?.fail({ error });
|
|
648
|
+
},
|
|
649
|
+
next: async ({ events }) => {
|
|
650
|
+
clearTimeout(timeout);
|
|
651
|
+
subscription.unsubscribe();
|
|
652
|
+
await auditorEvent?.success();
|
|
653
|
+
res.json(events);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
req.on("close", () => {
|
|
657
|
+
subscription.unsubscribe();
|
|
658
|
+
clearTimeout(timeout);
|
|
659
|
+
});
|
|
660
|
+
} catch (err) {
|
|
661
|
+
await auditorEvent?.fail({ error: err });
|
|
662
|
+
throw err;
|
|
549
663
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
664
|
+
}).post("/v2/dry-run", async (req, res) => {
|
|
665
|
+
const auditorEvent = await auditor?.createEvent({
|
|
666
|
+
eventId: "task",
|
|
667
|
+
request: req,
|
|
668
|
+
meta: {
|
|
669
|
+
actionType: "dry-run"
|
|
670
|
+
}
|
|
553
671
|
});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
672
|
+
try {
|
|
673
|
+
const credentials = await httpAuth.credentials(req);
|
|
674
|
+
await checkPermissions.checkPermission({
|
|
675
|
+
credentials,
|
|
676
|
+
permissions: [alpha.taskCreatePermission],
|
|
677
|
+
permissionService: permissions
|
|
678
|
+
});
|
|
679
|
+
const bodySchema = z.z.object({
|
|
680
|
+
template: z.z.unknown(),
|
|
681
|
+
values: z.z.record(z.z.unknown()),
|
|
682
|
+
secrets: z.z.record(z.z.string()).optional(),
|
|
683
|
+
directoryContents: z.z.array(
|
|
684
|
+
z.z.object({ path: z.z.string(), base64Content: z.z.string() })
|
|
685
|
+
)
|
|
686
|
+
});
|
|
687
|
+
const body = await bodySchema.parseAsync(req.body).catch((e) => {
|
|
688
|
+
throw new errors.InputError(`Malformed request: ${e}`);
|
|
689
|
+
});
|
|
690
|
+
const template = body.template;
|
|
691
|
+
if (!await pluginScaffolderCommon.templateEntityV1beta3Validator.check(template)) {
|
|
692
|
+
throw new errors.InputError("Input template is not a template");
|
|
561
693
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
steps,
|
|
586
|
-
output: template.spec.output ?? {},
|
|
587
|
-
parameters: body.values,
|
|
588
|
-
user: {
|
|
589
|
-
entity: userEntity,
|
|
590
|
-
ref: userEntityRef
|
|
694
|
+
const { token } = await auth.getPluginRequestToken({
|
|
695
|
+
onBehalfOf: credentials,
|
|
696
|
+
targetPluginId: "catalog"
|
|
697
|
+
});
|
|
698
|
+
const userEntityRef = auth.isPrincipal(credentials, "user") ? credentials.principal.userEntityRef : void 0;
|
|
699
|
+
const userEntity = userEntityRef ? await catalogClient.getEntityByRef(userEntityRef, { token }) : void 0;
|
|
700
|
+
const templateRef = `${template.kind}:${template.metadata.namespace || "default"}/${template.metadata.name}`;
|
|
701
|
+
for (const parameters of [template.spec.parameters ?? []].flat()) {
|
|
702
|
+
const result2 = jsonschema.validate(body.values, parameters);
|
|
703
|
+
if (!result2.valid) {
|
|
704
|
+
await auditorEvent?.fail({
|
|
705
|
+
// TODO(Rugvip): Seems like there aren't proper types for AggregateError yet
|
|
706
|
+
error: AggregateError(
|
|
707
|
+
result2.errors,
|
|
708
|
+
"Could not execute dry run"
|
|
709
|
+
),
|
|
710
|
+
meta: {
|
|
711
|
+
templateRef,
|
|
712
|
+
parameters: template.spec.parameters
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
res.status(400).json({ errors: result2.errors });
|
|
716
|
+
return;
|
|
591
717
|
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}))
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
718
|
+
}
|
|
719
|
+
const steps = template.spec.steps.map((step, index) => ({
|
|
720
|
+
...step,
|
|
721
|
+
id: step.id ?? `step-${index + 1}`,
|
|
722
|
+
name: step.name ?? step.action
|
|
723
|
+
}));
|
|
724
|
+
const dryRunId = uuid.v4();
|
|
725
|
+
const contentsPath = backendPluginApi.resolveSafeChildPath(
|
|
726
|
+
workingDirectory,
|
|
727
|
+
`dry-run-content-${dryRunId}`
|
|
728
|
+
);
|
|
729
|
+
const templateInfo = {
|
|
730
|
+
entityRef: "template:default/dry-run",
|
|
731
|
+
entity: {
|
|
732
|
+
metadata: template.metadata
|
|
733
|
+
},
|
|
734
|
+
baseUrl: url.pathToFileURL(
|
|
735
|
+
backendPluginApi.resolveSafeChildPath(contentsPath, "template.yaml")
|
|
736
|
+
).toString()
|
|
737
|
+
};
|
|
738
|
+
const result = await dryRunner({
|
|
739
|
+
spec: {
|
|
740
|
+
apiVersion: template.apiVersion,
|
|
741
|
+
steps,
|
|
742
|
+
output: template.spec.output ?? {},
|
|
743
|
+
parameters: body.values,
|
|
744
|
+
user: {
|
|
745
|
+
entity: userEntity,
|
|
746
|
+
ref: userEntityRef
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
templateInfo,
|
|
750
|
+
directoryContents: (body.directoryContents ?? []).map((file) => ({
|
|
751
|
+
path: file.path,
|
|
752
|
+
content: Buffer.from(file.base64Content, "base64")
|
|
753
|
+
})),
|
|
754
|
+
secrets: {
|
|
755
|
+
...body.secrets,
|
|
756
|
+
...token && { backstageToken: token }
|
|
757
|
+
},
|
|
758
|
+
credentials
|
|
759
|
+
});
|
|
760
|
+
await auditorEvent?.success({
|
|
761
|
+
meta: {
|
|
762
|
+
templateRef,
|
|
763
|
+
parameters: template.spec.parameters
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
res.status(200).json({
|
|
767
|
+
...result,
|
|
768
|
+
steps,
|
|
769
|
+
directoryContents: result.directoryContents.map((file) => ({
|
|
770
|
+
path: file.path,
|
|
771
|
+
executable: file.executable,
|
|
772
|
+
base64Content: file.content.toString("base64")
|
|
773
|
+
}))
|
|
774
|
+
});
|
|
775
|
+
} catch (err) {
|
|
776
|
+
await auditorEvent?.fail({ error: err });
|
|
777
|
+
throw err;
|
|
778
|
+
}
|
|
613
779
|
}).post("/v2/autocomplete/:provider/:resource", async (req, res) => {
|
|
614
780
|
const { token, context } = req.body;
|
|
615
781
|
const { provider, resource } = req.params;
|