@pagerduty/backstage-plugin-backend 0.11.0 → 0.13.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,13 +1,15 @@
1
1
  'use strict';
2
2
 
3
- var backendCommon = require('@backstage/backend-common');
4
3
  var pagerduty = require('../apis/pagerduty.cjs.js');
5
- var dataLoader = require('../services/dataLoader.cjs.js');
6
- var matchingEngine = require('../services/matchingEngine.cjs.js');
4
+ var autoMatchRunner = require('../services/autoMatchRunner.cjs.js');
5
+ var autoMatchJobs = require('../services/autoMatchJobs.cjs.js');
7
6
  var backstagePluginCommon = require('@pagerduty/backstage-plugin-common');
8
7
  var auth = require('../auth/auth.cjs.js');
9
8
  var express = require('express');
10
9
  var Router = require('express-promise-router');
10
+ var mappingsController = require('../controllers/mappings-controller.cjs.js');
11
+ var catalogEntity = require('../utils/catalog-entity.cjs.js');
12
+ var rootHttpRouter = require('@backstage/backend-defaults/rootHttpRouter');
11
13
 
12
14
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
13
15
 
@@ -32,36 +34,6 @@ function _interopNamespaceCompat(e) {
32
34
  var express__namespace = /*#__PURE__*/_interopNamespaceCompat(express);
33
35
  var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
34
36
 
35
- async function createComponentEntitiesReferenceDict({
36
- items: componentEntities
37
- }) {
38
- const componentEntitiesDict = {};
39
- await Promise.all(
40
- componentEntities.map(async (entity) => {
41
- const serviceId = entity.metadata.annotations?.["pagerduty.com/service-id"];
42
- const integrationKey = entity.metadata.annotations?.["pagerduty.com/integration-key"];
43
- const account = entity.metadata.annotations?.["pagerduty.com/account"];
44
- if (serviceId !== void 0 && serviceId !== "") {
45
- componentEntitiesDict[serviceId] = {
46
- ref: `${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`.toLowerCase(),
47
- name: entity.metadata.name
48
- };
49
- } else if (integrationKey !== void 0 && integrationKey !== "") {
50
- const service = await pagerduty.getServiceByIntegrationKey(
51
- integrationKey,
52
- account
53
- ).catch(() => void 0);
54
- if (service !== void 0) {
55
- componentEntitiesDict[service.id] = {
56
- ref: `${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`.toLowerCase(),
57
- name: entity.metadata.name
58
- };
59
- }
60
- }
61
- })
62
- );
63
- return componentEntitiesDict;
64
- }
65
37
  async function buildEntityMappingsResponse(entityMappings, componentEntitiesDict, componentEntities, pagerDutyServices) {
66
38
  const result = {
67
39
  mappings: []
@@ -180,15 +152,19 @@ async function buildEntityMappingsResponse(entityMappings, componentEntitiesDict
180
152
  return result;
181
153
  }
182
154
  async function createRouter(options) {
183
- const { logger, config, store, catalogApi } = options;
184
- let { auth: auth$1 } = options;
185
- if (!auth$1) {
186
- auth$1 = backendCommon.createLegacyAuthAdapters(options).auth;
155
+ const { logger, config, store, catalogApi, cache } = options;
156
+ if (!catalogApi) {
157
+ throw new Error("Catalog API is required to start the PagerDuty plugin backend");
158
+ }
159
+ if (!cache) {
160
+ throw new Error("Cache service is required to start the PagerDuty plugin backend");
187
161
  }
188
162
  await auth.loadAuthConfig(config, logger);
189
163
  pagerduty.loadPagerDutyEndpointsFromConfig(config, logger);
190
164
  const router = Router__default.default();
191
165
  router.use(express__namespace.json());
166
+ const runAutoMatch = autoMatchRunner.createAutoMatchRunner(catalogApi);
167
+ const autoMatchJobs$1 = new autoMatchJobs.AutoMatchJobRegistry(cache, runAutoMatch);
192
168
  router.delete(
193
169
  "/dependencies/service/:serviceId",
194
170
  async (request, response) => {
@@ -439,6 +415,128 @@ async function createRouter(options) {
439
415
  response.status(error.status).json({
440
416
  errors: [`${error.message}`]
441
417
  });
418
+ } else {
419
+ logger.error(
420
+ `Unexpected error occurred while processing request: ${error}`
421
+ );
422
+ response.status(500).json({
423
+ errors: [error instanceof Error ? error.message : String(error)]
424
+ });
425
+ }
426
+ }
427
+ });
428
+ router.post("/mapping/entities/bulk", async (request, response) => {
429
+ try {
430
+ const { mappings } = request.body;
431
+ if (!Array.isArray(mappings)) {
432
+ response.status(400).json({
433
+ error: "Bad Request: 'mappings' must be an array"
434
+ });
435
+ return;
436
+ }
437
+ const existingMappings = await store.getAllEntityMappings();
438
+ const existingServiceIds = new Set(
439
+ existingMappings.map((m) => m.serviceId)
440
+ );
441
+ const newMappings = [];
442
+ const skipped = [];
443
+ const errors = [];
444
+ for (const entity of mappings) {
445
+ if (!entity.serviceId) {
446
+ errors.push({
447
+ entityRef: entity.entityRef,
448
+ error: "Missing serviceId"
449
+ });
450
+ continue;
451
+ }
452
+ if (existingServiceIds.has(entity.serviceId)) {
453
+ skipped.push(entity);
454
+ continue;
455
+ }
456
+ if (entity.entityRef !== "" && (entity.integrationKey === "" || entity.integrationKey === void 0)) {
457
+ try {
458
+ const backstageVendorId = "PRO19CT";
459
+ const service = await pagerduty.getServiceById(
460
+ entity.serviceId,
461
+ entity.account
462
+ );
463
+ const backstageIntegration = service.integrations?.find(
464
+ (integration) => integration.vendor?.id === backstageVendorId
465
+ );
466
+ if (!backstageIntegration) {
467
+ const integrationKey = await pagerduty.createServiceIntegration({
468
+ serviceId: entity.serviceId,
469
+ vendorId: backstageVendorId,
470
+ account: entity.account
471
+ });
472
+ entity.integrationKey = integrationKey;
473
+ } else {
474
+ entity.integrationKey = backstageIntegration.integration_key;
475
+ }
476
+ } catch (error) {
477
+ errors.push({
478
+ entityRef: entity.entityRef,
479
+ serviceId: entity.serviceId,
480
+ error: error instanceof Error ? `Failed to create integration: ${error.message}` : "Failed to create integration"
481
+ });
482
+ continue;
483
+ }
484
+ }
485
+ newMappings.push(entity);
486
+ }
487
+ let insertedIds = [];
488
+ if (newMappings.length > 0) {
489
+ try {
490
+ insertedIds = await store.bulkInsertEntityMappings(newMappings);
491
+ await Promise.all(
492
+ newMappings.map(async (entity) => {
493
+ if (entity.entityRef !== "") {
494
+ await catalogApi?.refreshEntity(entity.entityRef);
495
+ }
496
+ })
497
+ );
498
+ } catch (error) {
499
+ logger.error(`Bulk insert failed: ${error}`);
500
+ response.status(500).json({
501
+ errors: ["Bulk insert failed"]
502
+ });
503
+ return;
504
+ }
505
+ }
506
+ const results = newMappings.map((entity, index) => ({
507
+ id: insertedIds[index],
508
+ entityRef: entity.entityRef,
509
+ integrationKey: entity.integrationKey,
510
+ serviceId: entity.serviceId,
511
+ status: entity.status,
512
+ account: entity.account
513
+ }));
514
+ response.json({
515
+ success: results,
516
+ skipped: skipped.map((entity) => ({
517
+ entityRef: entity.entityRef,
518
+ serviceId: entity.serviceId,
519
+ reason: "Mapping already exists for this service ID"
520
+ })),
521
+ errors,
522
+ total: mappings.length,
523
+ successCount: results.length,
524
+ skippedCount: skipped.length,
525
+ errorCount: errors.length
526
+ });
527
+ } catch (error) {
528
+ if (error instanceof backstagePluginCommon.HttpError) {
529
+ logger.error(
530
+ `Error occurred while processing bulk mappings: ${error.message}`
531
+ );
532
+ response.status(error.status).json({
533
+ errors: [`${error.message}`]
534
+ });
535
+ } else {
536
+ logger.error(`Unexpected error: ${error}`);
537
+ response.status(500).json({
538
+ errors: ["Internal server error"]
539
+ });
442
540
  }
443
541
  }
444
542
  });
@@ -450,7 +548,7 @@ async function createRouter(options) {
450
548
  kind: "Component"
451
549
  }
452
550
  });
453
- const componentEntitiesDict = await createComponentEntitiesReferenceDict(componentEntities);
551
+ const componentEntitiesDict = await catalogEntity.createComponentEntitiesReferenceDict(componentEntities);
454
552
  const pagerDutyServices = await pagerduty.getAllServices();
455
553
  const result = await buildEntityMappingsResponse(
456
554
  entityMappings,
@@ -467,6 +565,7 @@ async function createRouter(options) {
467
565
  }
468
566
  }
469
567
  });
568
+ router.post("/mapping/entities", mappingsController.getMappingEntities(store, catalogApi));
470
569
  router.get(
471
570
  "/mapping/entity/:type/:namespace/:name",
472
571
  async (request, response) => {
@@ -526,90 +625,45 @@ async function createRouter(options) {
526
625
  }
527
626
  }
528
627
  );
529
- router.post("/mapping/entity/auto-match", async (request, response) => {
530
- try {
531
- const threshold = request.body.threshold ?? 100;
532
- if (typeof threshold !== "number" || threshold < 0 || threshold > 100) {
533
- response.status(400).json({
534
- error: "Invalid threshold. Must be a number between 0 and 100."
535
- });
536
- return;
537
- }
538
- const bestOnly = request.body.bestOnly ?? false;
539
- const loadStartTime = Date.now();
540
- const { pdServices, bsComponents } = await dataLoader.loadBothSources({
541
- catalogApi
628
+ router.post("/mapping/entity/auto-match/start", async (request, response) => {
629
+ const threshold = request.body.threshold ?? 100;
630
+ if (typeof threshold !== "number" || threshold < 0 || threshold > 100) {
631
+ response.status(400).json({
632
+ error: "Invalid threshold. Must be a number between 0 and 100."
542
633
  });
543
- const loadTime = Date.now() - loadStartTime;
544
- const matchStartTime = Date.now();
545
- const matchingConfig = { threshold };
546
- let matches = matchingEngine.findMatches(pdServices, bsComponents, matchingConfig);
547
- if (bestOnly) {
548
- matches = matchingEngine.filterToBestMatchPerService(matches);
549
- }
550
- const matchTime = Date.now() - matchStartTime;
551
- const totalComparisons = pdServices.length * bsComponents.length;
552
- const exactMatches = matches.filter((m) => m.score === 100).length;
553
- const highConfidence = matches.filter(
554
- (m) => m.score >= 90 && m.score < 100
555
- ).length;
556
- const mediumConfidence = matches.filter(
557
- (m) => m.score >= 80 && m.score < 90
558
- ).length;
559
- const getConfidenceLevel = (score) => {
560
- if (score === 100) return "exact";
561
- if (score >= 90) return "high";
562
- if (score >= 80) return "medium";
563
- return "low";
564
- };
565
- response.json({
566
- matches: matches.map((m) => ({
567
- pagerDutyService: {
568
- serviceId: m.pagerDutyService.sourceId,
569
- name: m.pagerDutyService.rawName,
570
- team: m.pagerDutyService.teamName
571
- },
572
- backstageComponent: {
573
- entityRef: m.backstageComponent.sourceId,
574
- name: m.backstageComponent.rawName,
575
- owner: m.backstageComponent.teamName
576
- },
577
- score: m.score,
578
- confidence: getConfidenceLevel(m.score),
579
- scoreBreakdown: m.scoreBreakdown
580
- })),
581
- statistics: {
582
- totalPagerDutyServices: pdServices.length,
583
- totalBackstageComponents: bsComponents.length,
584
- totalPossibleComparisons: totalComparisons,
585
- matchesFound: matches.length,
586
- exactMatches,
587
- highConfidenceMatches: highConfidence,
588
- mediumConfidenceMatches: mediumConfidence,
589
- threshold,
590
- loadTimeMs: loadTime,
591
- matchTimeMs: matchTime,
592
- totalTimeMs: loadTime + matchTime
593
- }
634
+ return;
635
+ }
636
+ const bestOnly = request.body.bestOnly ?? false;
637
+ const team = request.body.team;
638
+ const account = request.body.account;
639
+ const job = await autoMatchJobs$1.start({
640
+ threshold,
641
+ bestOnly,
642
+ team,
643
+ account
644
+ });
645
+ response.status(202).json({
646
+ jobId: job.id,
647
+ status: job.status
648
+ });
649
+ });
650
+ router.get("/mapping/entity/auto-match/:jobId", async (request, response) => {
651
+ const jobId = request.params.jobId;
652
+ const job = await autoMatchJobs$1.get(jobId);
653
+ if (!job) {
654
+ response.status(404).json({
655
+ error: `Auto-match job ${jobId} not found.`
594
656
  });
595
- } catch (error) {
596
- logger.error(`Auto-match failed: ${error}`);
597
- if (error instanceof backstagePluginCommon.HttpError) {
598
- response.status(error.status).json({
599
- errors: [`${error.message}`]
600
- });
601
- } else if (error instanceof dataLoader.ServiceLoadError) {
602
- response.status(503).json({
603
- error: "Service temporarily unavailable",
604
- message: error.message
605
- });
606
- } else {
607
- response.status(500).json({
608
- error: "Auto-match failed",
609
- message: error instanceof Error ? error.message : String(error)
610
- });
611
- }
657
+ return;
612
658
  }
659
+ response.json({
660
+ jobId: job.id,
661
+ status: job.status,
662
+ createdAt: job.createdAt,
663
+ completedAt: job.completedAt,
664
+ result: job.result,
665
+ error: job.error
666
+ });
613
667
  });
614
668
  router.get("/escalation_policies", async (_, response) => {
615
669
  try {
@@ -685,31 +739,65 @@ async function createRouter(options) {
685
739
  }
686
740
  }
687
741
  });
742
+ router.get("/teams", async (request, response) => {
743
+ try {
744
+ const account = request.query.account;
745
+ const teams = await pagerduty.getAllTeams(account);
746
+ response.json(teams);
747
+ } catch (error) {
748
+ if (error instanceof backstagePluginCommon.HttpError) {
749
+ response.status(error.status).json({
750
+ errors: [`${error.message}`]
751
+ });
752
+ }
753
+ }
754
+ });
688
755
  router.get("/services", async (request, response) => {
689
756
  try {
690
- const integrationKey = request.query.integration_key || "";
691
- const account = request.query.account || "";
692
- if (integrationKey !== "") {
757
+ const integrationKey = request.query.integration_key;
758
+ const teamId = request.query.team_id;
759
+ const query = request.query.query;
760
+ const limit = request.query.limit ? parseInt(request.query.limit, 10) : void 0;
761
+ const account = request.query.account;
762
+ if (integrationKey) {
693
763
  const service = await pagerduty.getServiceByIntegrationKey(
694
764
  integrationKey,
695
- account
765
+ account || ""
696
766
  );
697
767
  const serviceResponse = {
698
768
  service
699
769
  };
700
770
  response.json(serviceResponse);
701
- } else {
702
- const services = await pagerduty.getAllServices();
703
- const servicesResponse = {
704
- services
705
- };
706
- response.json(servicesResponse);
771
+ return;
707
772
  }
773
+ if (teamId || query || limit) {
774
+ const teamIdsArray = teamId ? [teamId] : void 0;
775
+ const services2 = await pagerduty.getFilteredServices(
776
+ teamIdsArray,
777
+ query,
778
+ limit || 100,
779
+ account
780
+ );
781
+ response.json(services2);
782
+ return;
783
+ }
784
+ const services = await pagerduty.getAllServices();
785
+ const servicesResponse = {
786
+ services
787
+ };
788
+ response.json(servicesResponse);
708
789
  } catch (error) {
709
790
  if (error instanceof backstagePluginCommon.HttpError) {
710
791
  response.status(error.status).json({
711
792
  errors: [`${error.message}`]
712
793
  });
794
+ } else {
795
+ logger.error(
796
+ `Unexpected error occurred while processing request: ${error}`
797
+ );
798
+ response.status(500).json({
799
+ errors: [error instanceof Error ? error.message : String(error)]
800
+ });
713
801
  }
714
802
  }
715
803
  });
@@ -815,14 +903,30 @@ async function createRouter(options) {
815
903
  }
816
904
  }
817
905
  });
906
+ router.get("/accounts", async (_, response) => {
907
+ try {
908
+ const accountsConfig = config.getOptional("pagerDuty.accounts");
909
+ if (accountsConfig && accountsConfig.length > 0) {
910
+ const accounts = accountsConfig.map((account) => ({
911
+ id: account.id,
912
+ isDefault: account.isDefault || false
913
+ }));
914
+ response.status(200).json({ accounts });
915
+ } else {
916
+ response.status(200).json({ accounts: [] });
917
+ }
918
+ } catch (error) {
919
+ logger.error(`Failed to get accounts: ${error}`);
920
+ response.status(500).json({ error: "Failed to get accounts" });
921
+ }
922
+ });
818
923
  router.get("/health", async (_, response) => {
819
924
  response.status(200).json({ status: "ok" });
820
925
  });
821
- router.use(backendCommon.errorHandler());
926
+ router.use(rootHttpRouter.MiddlewareFactory.create({ config, logger }).error());
822
927
  return router;
823
928
  }
824
929
 
825
930
  exports.buildEntityMappingsResponse = buildEntityMappingsResponse;
826
- exports.createComponentEntitiesReferenceDict = createComponentEntitiesReferenceDict;
827
931
  exports.createRouter = createRouter;
828
932
  //# sourceMappingURL=router.cjs.js.map