@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.
- package/CHANGELOG.md +24 -0
- package/dist/apis/pagerduty.cjs.js +327 -64
- package/dist/apis/pagerduty.cjs.js.map +1 -1
- package/dist/auth/auth.cjs.js +5 -4
- package/dist/auth/auth.cjs.js.map +1 -1
- package/dist/controllers/mappings-controller.cjs.js +388 -0
- package/dist/controllers/mappings-controller.cjs.js.map +1 -0
- package/dist/db/PagerDutyBackendDatabase.cjs.js +16 -0
- package/dist/db/PagerDutyBackendDatabase.cjs.js.map +1 -1
- package/dist/index.cjs.js +0 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/service/router.cjs.js +280 -42
- package/dist/service/router.cjs.js.map +1 -1
- package/dist/services/dataLoader.cjs.js +73 -0
- package/dist/services/dataLoader.cjs.js.map +1 -0
- package/dist/services/matchingEngine.cjs.js +99 -0
- package/dist/services/matchingEngine.cjs.js.map +1 -0
- package/dist/services/pagerduty/index.cjs.js +11 -0
- package/dist/services/pagerduty/index.cjs.js.map +1 -0
- package/dist/utils/catalog-entity.cjs.js +50 -0
- package/dist/utils/catalog-entity.cjs.js.map +1 -0
- package/dist/utils/normalization.cjs.js +67 -0
- package/dist/utils/normalization.cjs.js.map +1 -0
- package/package.json +7 -6
|
@@ -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
|
|
605
|
-
|
|
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
|
-
|
|
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
|