@pagerduty/backstage-plugin-backend 0.10.3 → 0.12.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.
@@ -2,10 +2,14 @@
2
2
 
3
3
  var backendCommon = require('@backstage/backend-common');
4
4
  var pagerduty = require('../apis/pagerduty.cjs.js');
5
+ var dataLoader = require('../services/dataLoader.cjs.js');
6
+ var matchingEngine = require('../services/matchingEngine.cjs.js');
5
7
  var backstagePluginCommon = require('@pagerduty/backstage-plugin-common');
6
8
  var auth = require('../auth/auth.cjs.js');
7
9
  var express = require('express');
8
10
  var Router = require('express-promise-router');
11
+ var mappingsController = require('../controllers/mappings-controller.cjs.js');
12
+ var catalogEntity = require('../utils/catalog-entity.cjs.js');
9
13
 
10
14
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
11
15
 
@@ -30,36 +34,6 @@ function _interopNamespaceCompat(e) {
30
34
  var express__namespace = /*#__PURE__*/_interopNamespaceCompat(express);
31
35
  var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
32
36
 
33
- async function createComponentEntitiesReferenceDict({
34
- items: componentEntities
35
- }) {
36
- const componentEntitiesDict = {};
37
- await Promise.all(
38
- componentEntities.map(async (entity) => {
39
- const serviceId = entity.metadata.annotations?.["pagerduty.com/service-id"];
40
- const integrationKey = entity.metadata.annotations?.["pagerduty.com/integration-key"];
41
- const account = entity.metadata.annotations?.["pagerduty.com/account"];
42
- if (serviceId !== void 0 && serviceId !== "") {
43
- componentEntitiesDict[serviceId] = {
44
- ref: `${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`.toLowerCase(),
45
- name: entity.metadata.name
46
- };
47
- } else if (integrationKey !== void 0 && integrationKey !== "") {
48
- const service = await pagerduty.getServiceByIntegrationKey(
49
- integrationKey,
50
- account
51
- ).catch(() => void 0);
52
- if (service !== void 0) {
53
- componentEntitiesDict[service.id] = {
54
- ref: `${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`.toLowerCase(),
55
- name: entity.metadata.name
56
- };
57
- }
58
- }
59
- })
60
- );
61
- return componentEntitiesDict;
62
- }
63
37
  async function buildEntityMappingsResponse(entityMappings, componentEntitiesDict, componentEntities, pagerDutyServices) {
64
38
  const result = {
65
39
  mappings: []
@@ -183,6 +157,9 @@ async function createRouter(options) {
183
157
  if (!auth$1) {
184
158
  auth$1 = backendCommon.createLegacyAuthAdapters(options).auth;
185
159
  }
160
+ if (!catalogApi) {
161
+ throw new Error("Catalog API is required to start the PagerDuty plugin backend");
162
+ }
186
163
  await auth.loadAuthConfig(config, logger);
187
164
  pagerduty.loadPagerDutyEndpointsFromConfig(config, logger);
188
165
  const router = Router__default.default();
@@ -437,6 +414,128 @@ async function createRouter(options) {
437
414
  response.status(error.status).json({
438
415
  errors: [`${error.message}`]
439
416
  });
417
+ } else {
418
+ logger.error(
419
+ `Unexpected error occurred while processing request: ${error}`
420
+ );
421
+ response.status(500).json({
422
+ errors: [error instanceof Error ? error.message : String(error)]
423
+ });
424
+ }
425
+ }
426
+ });
427
+ router.post("/mapping/entities/bulk", async (request, response) => {
428
+ try {
429
+ const { mappings } = request.body;
430
+ if (!Array.isArray(mappings)) {
431
+ response.status(400).json({
432
+ error: "Bad Request: 'mappings' must be an array"
433
+ });
434
+ return;
435
+ }
436
+ const existingMappings = await store.getAllEntityMappings();
437
+ const existingServiceIds = new Set(
438
+ existingMappings.map((m) => m.serviceId)
439
+ );
440
+ const newMappings = [];
441
+ const skipped = [];
442
+ const errors = [];
443
+ for (const entity of mappings) {
444
+ if (!entity.serviceId) {
445
+ errors.push({
446
+ entityRef: entity.entityRef,
447
+ error: "Missing serviceId"
448
+ });
449
+ continue;
450
+ }
451
+ if (existingServiceIds.has(entity.serviceId)) {
452
+ skipped.push(entity);
453
+ continue;
454
+ }
455
+ if (entity.entityRef !== "" && (entity.integrationKey === "" || entity.integrationKey === void 0)) {
456
+ try {
457
+ const backstageVendorId = "PRO19CT";
458
+ const service = await pagerduty.getServiceById(
459
+ entity.serviceId,
460
+ entity.account
461
+ );
462
+ const backstageIntegration = service.integrations?.find(
463
+ (integration) => integration.vendor?.id === backstageVendorId
464
+ );
465
+ if (!backstageIntegration) {
466
+ const integrationKey = await pagerduty.createServiceIntegration({
467
+ serviceId: entity.serviceId,
468
+ vendorId: backstageVendorId,
469
+ account: entity.account
470
+ });
471
+ entity.integrationKey = integrationKey;
472
+ } else {
473
+ entity.integrationKey = backstageIntegration.integration_key;
474
+ }
475
+ } catch (error) {
476
+ errors.push({
477
+ entityRef: entity.entityRef,
478
+ serviceId: entity.serviceId,
479
+ error: error instanceof Error ? `Failed to create integration: ${error.message}` : "Failed to create integration"
480
+ });
481
+ continue;
482
+ }
483
+ }
484
+ newMappings.push(entity);
485
+ }
486
+ let insertedIds = [];
487
+ if (newMappings.length > 0) {
488
+ try {
489
+ insertedIds = await store.bulkInsertEntityMappings(newMappings);
490
+ await Promise.all(
491
+ newMappings.map(async (entity) => {
492
+ if (entity.entityRef !== "") {
493
+ await catalogApi?.refreshEntity(entity.entityRef);
494
+ }
495
+ })
496
+ );
497
+ } catch (error) {
498
+ logger.error(`Bulk insert failed: ${error}`);
499
+ response.status(500).json({
500
+ errors: ["Bulk insert failed"]
501
+ });
502
+ return;
503
+ }
504
+ }
505
+ const results = newMappings.map((entity, index) => ({
506
+ id: insertedIds[index],
507
+ entityRef: entity.entityRef,
508
+ integrationKey: entity.integrationKey,
509
+ serviceId: entity.serviceId,
510
+ status: entity.status,
511
+ account: entity.account
512
+ }));
513
+ response.json({
514
+ success: results,
515
+ skipped: skipped.map((entity) => ({
516
+ entityRef: entity.entityRef,
517
+ serviceId: entity.serviceId,
518
+ reason: "Mapping already exists for this service ID"
519
+ })),
520
+ errors,
521
+ total: mappings.length,
522
+ successCount: results.length,
523
+ skippedCount: skipped.length,
524
+ errorCount: errors.length
525
+ });
526
+ } catch (error) {
527
+ if (error instanceof backstagePluginCommon.HttpError) {
528
+ logger.error(
529
+ `Error occurred while processing bulk mappings: ${error.message}`
530
+ );
531
+ response.status(error.status).json({
532
+ errors: [`${error.message}`]
533
+ });
534
+ } else {
535
+ logger.error(`Unexpected error: ${error}`);
536
+ response.status(500).json({
537
+ errors: ["Internal server error"]
538
+ });
440
539
  }
441
540
  }
442
541
  });
@@ -448,7 +547,7 @@ async function createRouter(options) {
448
547
  kind: "Component"
449
548
  }
450
549
  });
451
- const componentEntitiesDict = await createComponentEntitiesReferenceDict(componentEntities);
550
+ const componentEntitiesDict = await catalogEntity.createComponentEntitiesReferenceDict(componentEntities);
452
551
  const pagerDutyServices = await pagerduty.getAllServices();
453
552
  const result = await buildEntityMappingsResponse(
454
553
  entityMappings,
@@ -465,6 +564,7 @@ async function createRouter(options) {
465
564
  }
466
565
  }
467
566
  });
567
+ router.post("/mapping/entities", mappingsController.getMappingEntities(store, catalogApi));
468
568
  router.get(
469
569
  "/mapping/entity/:type/:namespace/:name",
470
570
  async (request, response) => {
@@ -524,6 +624,94 @@ async function createRouter(options) {
524
624
  }
525
625
  }
526
626
  );
627
+ router.post("/mapping/entity/auto-match", async (request, response) => {
628
+ try {
629
+ const threshold = request.body.threshold ?? 100;
630
+ const account = request.body.account;
631
+ if (typeof threshold !== "number" || threshold < 0 || threshold > 100) {
632
+ response.status(400).json({
633
+ error: "Invalid threshold. Must be a number between 0 and 100."
634
+ });
635
+ return;
636
+ }
637
+ const bestOnly = request.body.bestOnly ?? false;
638
+ const loadStartTime = Date.now();
639
+ const { pdServices, bsComponents } = await dataLoader.loadBothSources({
640
+ catalogApi
641
+ });
642
+ const filteredPdServices = account ? pdServices.filter((service) => service.account === account) : pdServices;
643
+ const loadTime = Date.now() - loadStartTime;
644
+ const matchStartTime = Date.now();
645
+ const matchingConfig = { threshold };
646
+ let matches = matchingEngine.findMatches(filteredPdServices, bsComponents, matchingConfig);
647
+ if (bestOnly) {
648
+ matches = matchingEngine.filterToBestMatchPerService(matches);
649
+ }
650
+ const matchTime = Date.now() - matchStartTime;
651
+ const totalComparisons = filteredPdServices.length * bsComponents.length;
652
+ const exactMatches = matches.filter((m) => m.score === 100).length;
653
+ const highConfidence = matches.filter(
654
+ (m) => m.score >= 90 && m.score < 100
655
+ ).length;
656
+ const mediumConfidence = matches.filter(
657
+ (m) => m.score >= 80 && m.score < 90
658
+ ).length;
659
+ const getConfidenceLevel = (score) => {
660
+ if (score === 100) return "exact";
661
+ if (score >= 90) return "high";
662
+ if (score >= 80) return "medium";
663
+ return "low";
664
+ };
665
+ response.json({
666
+ matches: matches.map((m) => ({
667
+ pagerDutyService: {
668
+ serviceId: m.pagerDutyService.sourceId,
669
+ name: m.pagerDutyService.rawName,
670
+ team: m.pagerDutyService.teamName,
671
+ account: m.pagerDutyService.account
672
+ },
673
+ backstageComponent: {
674
+ entityRef: m.backstageComponent.sourceId,
675
+ name: m.backstageComponent.rawName,
676
+ owner: m.backstageComponent.teamName
677
+ },
678
+ score: m.score,
679
+ confidence: getConfidenceLevel(m.score),
680
+ scoreBreakdown: m.scoreBreakdown
681
+ })),
682
+ statistics: {
683
+ totalPagerDutyServices: filteredPdServices.length,
684
+ totalBackstageComponents: bsComponents.length,
685
+ totalPossibleComparisons: totalComparisons,
686
+ matchesFound: matches.length,
687
+ exactMatches,
688
+ highConfidenceMatches: highConfidence,
689
+ mediumConfidenceMatches: mediumConfidence,
690
+ threshold,
691
+ loadTimeMs: loadTime,
692
+ matchTimeMs: matchTime,
693
+ totalTimeMs: loadTime + matchTime
694
+ }
695
+ });
696
+ } catch (error) {
697
+ logger.error(`Auto-match failed: ${error}`);
698
+ if (error instanceof backstagePluginCommon.HttpError) {
699
+ response.status(error.status).json({
700
+ errors: [`${error.message}`]
701
+ });
702
+ } else if (error instanceof dataLoader.ServiceLoadError) {
703
+ response.status(503).json({
704
+ error: "Service temporarily unavailable",
705
+ message: error.message
706
+ });
707
+ } else {
708
+ response.status(500).json({
709
+ error: "Auto-match failed",
710
+ message: error instanceof Error ? error.message : String(error)
711
+ });
712
+ }
713
+ }
714
+ });
527
715
  router.get("/escalation_policies", async (_, response) => {
528
716
  try {
529
717
  let escalationPolicyList = await pagerduty.getAllEscalationPolicies();
@@ -598,31 +786,65 @@ async function createRouter(options) {
598
786
  }
599
787
  }
600
788
  });
789
+ router.get("/teams", async (request, response) => {
790
+ try {
791
+ const account = request.query.account;
792
+ const teams = await pagerduty.getAllTeams(account);
793
+ response.json(teams);
794
+ } catch (error) {
795
+ if (error instanceof backstagePluginCommon.HttpError) {
796
+ response.status(error.status).json({
797
+ errors: [`${error.message}`]
798
+ });
799
+ }
800
+ }
801
+ });
601
802
  router.get("/services", async (request, response) => {
602
803
  try {
603
- const integrationKey = request.query.integration_key || "";
604
- const account = request.query.account || "";
605
- if (integrationKey !== "") {
804
+ const integrationKey = request.query.integration_key;
805
+ const teamId = request.query.team_id;
806
+ const query = request.query.query;
807
+ const limit = request.query.limit ? parseInt(request.query.limit, 10) : void 0;
808
+ const account = request.query.account;
809
+ if (integrationKey) {
606
810
  const service = await pagerduty.getServiceByIntegrationKey(
607
811
  integrationKey,
608
- account
812
+ account || ""
609
813
  );
610
814
  const serviceResponse = {
611
815
  service
612
816
  };
613
817
  response.json(serviceResponse);
614
- } else {
615
- const services = await pagerduty.getAllServices();
616
- const servicesResponse = {
617
- services
618
- };
619
- response.json(servicesResponse);
818
+ return;
620
819
  }
820
+ if (teamId || query || limit) {
821
+ const teamIdsArray = teamId ? [teamId] : void 0;
822
+ const services2 = await pagerduty.getFilteredServices(
823
+ teamIdsArray,
824
+ query,
825
+ limit || 100,
826
+ account
827
+ );
828
+ response.json(services2);
829
+ return;
830
+ }
831
+ const services = await pagerduty.getAllServices();
832
+ const servicesResponse = {
833
+ services
834
+ };
835
+ response.json(servicesResponse);
621
836
  } catch (error) {
622
837
  if (error instanceof backstagePluginCommon.HttpError) {
623
838
  response.status(error.status).json({
624
839
  errors: [`${error.message}`]
625
840
  });
841
+ } else {
842
+ logger.error(
843
+ `Unexpected error occurred while processing request: ${error}`
844
+ );
845
+ response.status(500).json({
846
+ errors: [error instanceof Error ? error.message : String(error)]
847
+ });
626
848
  }
627
849
  }
628
850
  });
@@ -728,6 +950,23 @@ async function createRouter(options) {
728
950
  }
729
951
  }
730
952
  });
953
+ router.get("/accounts", async (_, response) => {
954
+ try {
955
+ const accountsConfig = config.getOptional("pagerDuty.accounts");
956
+ if (accountsConfig && accountsConfig.length > 0) {
957
+ const accounts = accountsConfig.map((account) => ({
958
+ id: account.id,
959
+ isDefault: account.isDefault || false
960
+ }));
961
+ response.status(200).json({ accounts });
962
+ } else {
963
+ response.status(200).json({ accounts: [] });
964
+ }
965
+ } catch (error) {
966
+ logger.error(`Failed to get accounts: ${error}`);
967
+ response.status(500).json({ error: "Failed to get accounts" });
968
+ }
969
+ });
731
970
  router.get("/health", async (_, response) => {
732
971
  response.status(200).json({ status: "ok" });
733
972
  });
@@ -736,6 +975,5 @@ async function createRouter(options) {
736
975
  }
737
976
 
738
977
  exports.buildEntityMappingsResponse = buildEntityMappingsResponse;
739
- exports.createComponentEntitiesReferenceDict = createComponentEntitiesReferenceDict;
740
978
  exports.createRouter = createRouter;
741
979
  //# sourceMappingURL=router.cjs.js.map