@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.
@@ -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
- req.params,
368
+ { kind, namespace, name },
279
369
  token,
280
370
  credentials
281
371
  );
282
- const parameters = [template.spec.parameters ?? []].flat();
283
- const presentation = template.spec.presentation;
284
- res.json({
285
- title: template.metadata.title ?? template.metadata.name,
286
- ...presentation ? { presentation } : {},
287
- description: template.metadata.description,
288
- "ui:options": template.metadata["ui:options"],
289
- steps: parameters.map((schema) => ({
290
- title: schema.title ?? "Please enter the following information",
291
- description: schema.description,
292
- schema
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
- EXPERIMENTAL_formDecorators: template.spec.EXPERIMENTAL_formDecorators
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/actions", async (_req, res) => {
298
- const actionsList = actionRegistry.list().map((action) => {
299
- return {
300
- id: action.id,
301
- description: action.description,
302
- examples: action.examples,
303
- schema: action.schema
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
- const userEntityRef = auth.isPrincipal(credentials, "user") ? credentials.principal.userEntityRef : void 0;
323
- const userEntity = userEntityRef ? await catalogClient.getEntityByRef(userEntityRef, { token }) : void 0;
324
- let auditLog = `Scaffolding task for ${templateRef}`;
325
- if (userEntityRef) {
326
- auditLog += ` created by ${userEntityRef}`;
327
- }
328
- logger.info(auditLog);
329
- const values = req.body.values;
330
- const template = await authorizeTemplate(
331
- { kind, namespace, name },
332
- token,
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
- const baseUrl = helpers.getEntityBaseUrl(template);
343
- const taskSpec = {
344
- apiVersion: template.apiVersion,
345
- steps: template.spec.steps.map((step, index) => ({
346
- ...step,
347
- id: step.id ?? `step-${index + 1}`,
348
- name: step.name ?? step.action
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
- const secrets = {
366
- ...req.body.secrets,
367
- backstageToken: token,
368
- __initiatorCredentials: JSON.stringify({
369
- ...credentials,
370
- // credentials.token is nonenumerable and will not be serialized, so we need to add it explicitly
371
- token: credentials.token
372
- })
373
- };
374
- const result = await taskBroker.dispatch({
375
- spec: taskSpec,
376
- createdBy: userEntityRef,
377
- secrets
378
- });
379
- res.status(201).json({ id: result.taskId });
380
- }).get("/v2/tasks", async (req, res) => {
381
- const credentials = await httpAuth.credentials(req);
382
- await checkPermissions.checkPermission({
383
- credentials,
384
- permissions: [alpha.taskReadPermission],
385
- permissionService: permissions
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
- const createdBy = helpers.parseStringsParam(req.query.createdBy, "createdBy");
393
- const status = helpers.parseStringsParam(req.query.status, "status");
394
- const order = helpers.parseStringsParam(req.query.order, "order")?.map((item) => {
395
- const match = item.match(/^(asc|desc):(.+)$/);
396
- if (!match) {
397
- throw new errors.InputError(
398
- `Invalid order parameter "${item}", expected "<asc or desc>:<field name>"`
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
- const limit = helpers.parseNumberParam(req.query.limit, "limit");
407
- const offset = helpers.parseNumberParam(req.query.offset, "offset");
408
- const tasks = await taskBroker.list({
409
- filters: {
410
- createdBy,
411
- status: status ? status : void 0
412
- },
413
- order,
414
- pagination: {
415
- limit: limit ? limit[0] : void 0,
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
- res.status(200).json(tasks);
420
- }).get("/v2/tasks/:taskId", async (req, res) => {
421
- const credentials = await httpAuth.credentials(req);
422
- await checkPermissions.checkPermission({
423
- credentials,
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 taskBroker.cancel?.(taskId);
443
- res.status(200).json({ status: "cancelled" });
444
- }).post("/v2/tasks/:taskId/retry", async (req, res) => {
445
- const credentials = await httpAuth.credentials(req);
446
- await checkPermissions.checkPermission({
447
- credentials,
448
- permissions: [alpha.taskCreatePermission, alpha.taskReadPermission],
449
- permissionService: permissions
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 taskBroker.retry?.(taskId);
453
- res.status(201).json({ id: taskId });
454
- }).get("/v2/tasks/:taskId/eventstream", async (req, res) => {
455
- const credentials = await httpAuth.credentials(req);
456
- await checkPermissions.checkPermission({
457
- credentials,
458
- permissions: [alpha.taskReadPermission],
459
- permissionService: permissions
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 after = req.query.after !== void 0 ? Number(req.query.after) : void 0;
463
- logger.debug(`Event stream observing taskId '${taskId}' opened`);
464
- res.writeHead(200, {
465
- Connection: "keep-alive",
466
- "Cache-Control": "no-cache",
467
- "Content-Type": "text/event-stream"
562
+ const auditorEvent = await auditor?.createEvent({
563
+ eventId: "task",
564
+ request: req,
565
+ meta: {
566
+ actionType: "stream",
567
+ taskId
568
+ }
468
569
  });
469
- const subscription = taskBroker.event$({ taskId, after }).subscribe({
470
- error: (error) => {
471
- logger.error(
472
- `Received error from event stream when observing taskId '${taskId}', ${error}`
473
- );
474
- res.end();
475
- },
476
- next: ({ events }) => {
477
- let shouldUnsubscribe = false;
478
- for (const event of events) {
479
- res.write(
480
- `event: ${event.type}
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
- if (event.type === "completion" && !event.isTaskRecoverable) {
486
- shouldUnsubscribe = true;
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
- res.flush?.();
490
- if (shouldUnsubscribe) {
491
- subscription.unsubscribe();
492
- res.end();
493
- }
494
- }
495
- });
496
- req.on("close", () => {
497
- subscription.unsubscribe();
498
- logger.debug(`Event stream observing taskId '${taskId}' closed`);
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 after = Number(req.query.after) || void 0;
509
- const timeout = setTimeout(() => {
510
- res.json([]);
511
- }, 3e4);
512
- const subscription = taskBroker.event$({ taskId, after }).subscribe({
513
- error: (error) => {
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
- req.on("close", () => {
525
- subscription.unsubscribe();
526
- clearTimeout(timeout);
527
- });
528
- }).post("/v2/dry-run", async (req, res) => {
529
- const credentials = await httpAuth.credentials(req);
530
- await checkPermissions.checkPermission({
531
- credentials,
532
- permissions: [alpha.taskCreatePermission],
533
- permissionService: permissions
534
- });
535
- const bodySchema = z.z.object({
536
- template: z.z.unknown(),
537
- values: z.z.record(z.z.unknown()),
538
- secrets: z.z.record(z.z.string()).optional(),
539
- directoryContents: z.z.array(
540
- z.z.object({ path: z.z.string(), base64Content: z.z.string() })
541
- )
542
- });
543
- const body = await bodySchema.parseAsync(req.body).catch((e) => {
544
- throw new errors.InputError(`Malformed request: ${e}`);
545
- });
546
- const template = body.template;
547
- if (!await pluginScaffolderCommon.templateEntityV1beta3Validator.check(template)) {
548
- throw new errors.InputError("Input template is not a template");
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
- const { token } = await auth.getPluginRequestToken({
551
- onBehalfOf: credentials,
552
- targetPluginId: "catalog"
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
- const userEntityRef = auth.isPrincipal(credentials, "user") ? credentials.principal.userEntityRef : void 0;
555
- const userEntity = userEntityRef ? await catalogClient.getEntityByRef(userEntityRef, { token }) : void 0;
556
- for (const parameters of [template.spec.parameters ?? []].flat()) {
557
- const result2 = jsonschema.validate(body.values, parameters);
558
- if (!result2.valid) {
559
- res.status(400).json({ errors: result2.errors });
560
- return;
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
- const steps = template.spec.steps.map((step, index) => ({
564
- ...step,
565
- id: step.id ?? `step-${index + 1}`,
566
- name: step.name ?? step.action
567
- }));
568
- const dryRunId = uuid.v4();
569
- const contentsPath = backendPluginApi.resolveSafeChildPath(
570
- workingDirectory,
571
- `dry-run-content-${dryRunId}`
572
- );
573
- const templateInfo = {
574
- entityRef: catalogModel.stringifyEntityRef(template),
575
- entity: {
576
- metadata: template.metadata
577
- },
578
- baseUrl: url.pathToFileURL(
579
- backendPluginApi.resolveSafeChildPath(contentsPath, "template.yaml")
580
- ).toString()
581
- };
582
- const result = await dryRunner({
583
- spec: {
584
- apiVersion: template.apiVersion,
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
- templateInfo,
594
- directoryContents: (body.directoryContents ?? []).map((file) => ({
595
- path: file.path,
596
- content: Buffer.from(file.base64Content, "base64")
597
- })),
598
- secrets: {
599
- ...body.secrets,
600
- ...token && { backstageToken: token }
601
- },
602
- credentials
603
- });
604
- res.status(200).json({
605
- ...result,
606
- steps,
607
- directoryContents: result.directoryContents.map((file) => ({
608
- path: file.path,
609
- executable: file.executable,
610
- base64Content: file.content.toString("base64")
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;