@backstage-community/plugin-catalog-backend-module-apiiro-entity-processor 0.1.0 → 1.0.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 +22 -0
- package/README.md +76 -8
- package/dist/helpers/apiClient.cjs.js +120 -0
- package/dist/helpers/apiClient.cjs.js.map +1 -0
- package/dist/helpers/cacheManager.cjs.js +268 -0
- package/dist/helpers/cacheManager.cjs.js.map +1 -0
- package/dist/helpers/types.cjs.js +16 -0
- package/dist/helpers/types.cjs.js.map +1 -0
- package/dist/helpers/utils.cjs.js +48 -0
- package/dist/helpers/utils.cjs.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/module.cjs.js +12 -5
- package/dist/module.cjs.js.map +1 -1
- package/dist/processor/ApiiroAnnotationProcessor.cjs.js +56 -243
- package/dist/processor/ApiiroAnnotationProcessor.cjs.js.map +1 -1
- package/package.json +11 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @backstage-community/plugin-catalog-backend-module-apiiro-entity-processor
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- ff337ee: Added support to automatically apply application annotations to system entities.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [ff337ee]
|
|
12
|
+
- @backstage-community/plugin-apiiro-common@1.0.0
|
|
13
|
+
|
|
14
|
+
## 0.2.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- cd2ccac: Backstage version bump to v1.48.2
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [cd2ccac]
|
|
23
|
+
- @backstage-community/plugin-apiiro-common@0.2.0
|
|
24
|
+
|
|
3
25
|
## 0.1.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -6,12 +6,17 @@ Catalog backend module that automatically adds Apiiro annotations to Backstage e
|
|
|
6
6
|
|
|
7
7
|
When enabled, this module:
|
|
8
8
|
|
|
9
|
-
- Derives the Apiiro repository identifier from the entity's source location.
|
|
9
|
+
- Derives the Apiiro repository identifier from the entity's source location (for Component entities).
|
|
10
|
+
- Derives the Apiiro application identifier from the entity's reference and uid (for System entities, when applications view is enabled).
|
|
10
11
|
- Adds the following annotations when they are missing:
|
|
11
|
-
- `apiiro.com/repo-id`
|
|
12
|
+
- `apiiro.com/repo-id` (for Component entities)
|
|
13
|
+
- `apiiro.com/application-id` (for System entities)
|
|
12
14
|
- `apiiro.com/allow-metrics-view`
|
|
13
15
|
- **Does not overwrite** existing Apiiro annotations if they are already set on the entity.
|
|
14
16
|
|
|
17
|
+
> [!NOTE]
|
|
18
|
+
> Application annotation support requires the Backstage connector to be configured in Apiiro and `enableApplicationsView` to be set to `true` in the configuration.
|
|
19
|
+
|
|
15
20
|
This helps you avoid manually managing Apiiro annotations on every entity in your catalog.
|
|
16
21
|
|
|
17
22
|
## Installation
|
|
@@ -43,12 +48,20 @@ will be updated with Apiiro annotations when they are missing.
|
|
|
43
48
|
|
|
44
49
|
The processor works with the following annotations:
|
|
45
50
|
|
|
51
|
+
**For Component entities (repositories):**
|
|
52
|
+
|
|
46
53
|
- `apiiro.com/repo-id`: `<repo-key>`
|
|
47
|
-
- `apiiro.com/allow-metrics-view`: `"true"` or `"false"` (controls whether the Metrics view appears in the Apiiro
|
|
54
|
+
- `apiiro.com/allow-metrics-view`: `"true"` or `"false"` (controls whether the Metrics view appears in the Component Apiiro tab and Apiiro widget)
|
|
55
|
+
|
|
56
|
+
**For System entities (applications):**
|
|
57
|
+
|
|
58
|
+
- `apiiro.com/application-id`: `<application-key>`
|
|
59
|
+
- `apiiro.com/allow-metrics-view`: `"true"` or `"false"` (controls whether the Metrics view and repository list appears in the System Apiiro tab and Apiiro widget)
|
|
48
60
|
|
|
49
61
|
### Notes
|
|
50
62
|
|
|
51
|
-
-
|
|
63
|
+
- For Component entities, annotation values are derived from the value of `backstage.io/source-location`. If `backstage.io/source-location` is not present, Apiiro annotations will not be added.
|
|
64
|
+
- For System entities, the application identifier is derived from the entity reference and entity uid and matched against applications in Apiiro (requires `enableApplicationsView: true`).
|
|
52
65
|
- If Apiiro annotations already exist on an entity, they take precedence and will **not** be overwritten.
|
|
53
66
|
|
|
54
67
|
## Permissions and Metrics View
|
|
@@ -61,23 +74,78 @@ widgets. In `app-config.yaml` or `app-config.production.yaml`:
|
|
|
61
74
|
apiiro:
|
|
62
75
|
accessToken: ${APIIRO_TOKEN}
|
|
63
76
|
defaultAllowMetricsView: true
|
|
77
|
+
enableApplicationsView: false
|
|
64
78
|
# Optional configuration to allow or disallow metric views for specific entities
|
|
65
79
|
annotationControl:
|
|
66
80
|
entityNames:
|
|
67
81
|
- component:<namespace>/<entity-name>
|
|
82
|
+
- system:<namespace>/<entity-name>
|
|
68
83
|
exclude: true
|
|
69
84
|
```
|
|
70
85
|
|
|
71
|
-
Where the parameters
|
|
86
|
+
Where the configuration parameters are:
|
|
72
87
|
|
|
73
|
-
- `
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
88
|
+
- `defaultAllowMetricsView`: Default value for allowing metrics view (default: `true`).
|
|
89
|
+
- `enableApplicationsView`: Enables application annotation processing for System entities (default: `false`). **Note:** Requires Backstage connector configured in Apiiro.
|
|
90
|
+
- `annotationControl`:
|
|
91
|
+
- `entityNames`: List of entity references to control the metrics view access.
|
|
92
|
+
- `exclude: true` → **blocklist mode** (allow all entities except those listed).
|
|
93
|
+
- `exclude: false` → **allowlist mode** (deny all entities except those listed).
|
|
76
94
|
|
|
77
95
|
The `apiiro.com/allow-metrics-view` annotation and the above configuration
|
|
78
96
|
together determine whether a given entity can display metrics on Apiiro Tab and Apiiro Widget.
|
|
79
97
|
If you configure this list it will override the `defaultAllowMetricsView` configuration.
|
|
80
98
|
|
|
99
|
+
## High-Level Design
|
|
100
|
+
|
|
101
|
+
### Architecture Overview
|
|
102
|
+
|
|
103
|
+
The Apiiro Entity Processor is a catalog backend module that automatically enriches Backstage entities with Apiiro-specific annotations. The processor operates during catalog processing cycles and integrates with external Apiiro services to maintain consistent security metadata across your software ecosystem.
|
|
104
|
+
|
|
105
|
+
### Core Components
|
|
106
|
+
|
|
107
|
+
1. **ApiiroAnnotationProcessor**: Main catalog processor that handles entity annotation logic
|
|
108
|
+
2. **CacheManager**: Manages persistent caching of Apiiro data using Backstage's CacheService
|
|
109
|
+
3. **ApiiroApiClient**: Handles communication with Apiiro's REST APIs
|
|
110
|
+
4. **Entity Reference Resolution**: Maps Backstage entity references to Apiiro application identifiers
|
|
111
|
+
|
|
112
|
+
### Caching Strategy
|
|
113
|
+
|
|
114
|
+
#### Why CacheService (Persistent Caching)?
|
|
115
|
+
|
|
116
|
+
This implementation uses **Backstage's CacheService** for persistent data storage across multiple catalog runs. This decision is based on catalog module best practices and performance optimization requirements:
|
|
117
|
+
|
|
118
|
+
**Benefits of CacheService:**
|
|
119
|
+
|
|
120
|
+
- **Performance Optimization**: Reduces expensive external API calls to Apiiro (repository and application data)
|
|
121
|
+
- **Rate Limit Compliance**: Respects Apiiro API rate limits by minimizing requests
|
|
122
|
+
- **System Reliability**: Cached data provides resilience against network failures
|
|
123
|
+
- **Scalability**: Handles growing entity counts without degrading performance
|
|
124
|
+
- **Cross-Run Efficiency**: Multiple catalog runs (every refresh rate) reuse the same cached data
|
|
125
|
+
|
|
126
|
+
**Cached Data Types:**
|
|
127
|
+
|
|
128
|
+
- **Repository Mappings**: URL to Apiiro repository key mappings (1-hour TTL)
|
|
129
|
+
- **Application Mappings**: Entity UID to Apiiro application key mappings (1-hour TTL)
|
|
130
|
+
- **Entity References**: System entity reference to UID mappings (1-hour TTL)
|
|
131
|
+
|
|
132
|
+
### Data Flow
|
|
133
|
+
|
|
134
|
+
1. **Catalog Processing Trigger**: Backstage catalog runs every refresh rate (default: 5 minutes)
|
|
135
|
+
2. **Cache Check**: Processor checks CacheService for existing Apiiro data
|
|
136
|
+
3. **API Integration**: Fetches fresh data from Apiiro only when cache expires (1-hour TTL)
|
|
137
|
+
4. **Entity Processing**: Enriches entities with appropriate Apiiro annotations
|
|
138
|
+
5. **Cache Update**: Refreshes persistent cache with new data when needed
|
|
139
|
+
|
|
140
|
+
### Integration Points
|
|
141
|
+
|
|
142
|
+
- **Backstage Catalog**: Processes entities during standard catalog refresh cycles
|
|
143
|
+
- **Apiiro Repository API**: Maps source locations to repository identifiers
|
|
144
|
+
- **Apiiro Application API**: Maps system entities to application identifiers
|
|
145
|
+
- **Backstage Catalog API**: Resolves entity references to UIDs for system entity mapping
|
|
146
|
+
|
|
147
|
+
This design follows catalog module best practices by prioritizing performance, reliability, and efficient resource utilization while maintaining data freshness appropriate for security metadata.
|
|
148
|
+
|
|
81
149
|
## Development
|
|
82
150
|
|
|
83
151
|
This module is developed as part of the Apiiro Backstage integration.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fetch = require('node-fetch');
|
|
4
|
+
var pluginApiiroCommon = require('@backstage-community/plugin-apiiro-common');
|
|
5
|
+
var types = require('./types.cjs.js');
|
|
6
|
+
|
|
7
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
|
|
10
|
+
|
|
11
|
+
class ApiiroApiClient {
|
|
12
|
+
constructor(accessToken) {
|
|
13
|
+
this.accessToken = accessToken;
|
|
14
|
+
}
|
|
15
|
+
async fetchRepositoriesPage(pageCursor) {
|
|
16
|
+
const baseUrl = pluginApiiroCommon.APIIRO_DEFAULT_BASE_URL;
|
|
17
|
+
const params = new URLSearchParams();
|
|
18
|
+
params.append("pageSize", types.PAGE_LIMIT.toString());
|
|
19
|
+
if (pageCursor) {
|
|
20
|
+
params.append("next", pageCursor);
|
|
21
|
+
}
|
|
22
|
+
const url = `${baseUrl}/rest-api/v2/repositories?${params.toString()}`;
|
|
23
|
+
const response = await fetch__default.default(url, {
|
|
24
|
+
method: "GET",
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
27
|
+
"Content-Type": "application/json"
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const errorMessage = `Failed to fetch repositories from Apiiro API. Status: ${response.status} ${response.statusText}`;
|
|
32
|
+
throw new Error(errorMessage);
|
|
33
|
+
}
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
return {
|
|
36
|
+
items: data.items || [],
|
|
37
|
+
next: data.next || null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async fetchAllRepositories() {
|
|
41
|
+
const items = [];
|
|
42
|
+
let nextCursor = void 0;
|
|
43
|
+
let pageCount = 0;
|
|
44
|
+
do {
|
|
45
|
+
pageCount++;
|
|
46
|
+
if (pageCount > types.MAX_PAGES) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Pagination limit exceeded: Maximum of ${types.MAX_PAGES} pages allowed. This may indicate an infinite loop or an unexpectedly large dataset. Fetched ${items.length} repositories so far.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (!this.accessToken) {
|
|
52
|
+
console.warn(
|
|
53
|
+
"[ApiiroApiClient] Apiiro access token not configured. Please set apiiro.accessToken in your app-config."
|
|
54
|
+
);
|
|
55
|
+
return {
|
|
56
|
+
items: [],
|
|
57
|
+
totalCount: 0
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const page = await this.fetchRepositoriesPage(nextCursor ?? void 0);
|
|
61
|
+
items.push(...page.items);
|
|
62
|
+
nextCursor = page.next;
|
|
63
|
+
} while (nextCursor);
|
|
64
|
+
return { items, totalCount: items.length };
|
|
65
|
+
}
|
|
66
|
+
async fetchApplicationsPage(pageCursor) {
|
|
67
|
+
const baseUrl = pluginApiiroCommon.APIIRO_DEFAULT_BASE_URL;
|
|
68
|
+
const params = new URLSearchParams();
|
|
69
|
+
params.append("pageSize", types.PAGE_LIMIT.toString());
|
|
70
|
+
if (pageCursor) {
|
|
71
|
+
params.append("next", pageCursor);
|
|
72
|
+
}
|
|
73
|
+
const url = `${baseUrl}/rest-api/v1/applications/profiles?${params.toString()}`;
|
|
74
|
+
const response = await fetch__default.default(url, {
|
|
75
|
+
method: "GET",
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
78
|
+
"Content-Type": "application/json"
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const errorMessage = `Failed to fetch applications from Apiiro API. Status: ${response.status} ${response.statusText}`;
|
|
83
|
+
throw new Error(errorMessage);
|
|
84
|
+
}
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
return {
|
|
87
|
+
items: data.items || [],
|
|
88
|
+
next: data.next || null
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async fetchAllApplications() {
|
|
92
|
+
const items = [];
|
|
93
|
+
let nextCursor = void 0;
|
|
94
|
+
let pageCount = 0;
|
|
95
|
+
do {
|
|
96
|
+
pageCount++;
|
|
97
|
+
if (pageCount > types.MAX_PAGES) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Pagination limit exceeded: Maximum of ${types.MAX_PAGES} pages allowed. This may indicate an infinite loop or an unexpectedly large dataset. Fetched ${items.length} applications so far.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (!this.accessToken) {
|
|
103
|
+
console.warn(
|
|
104
|
+
"[ApiiroApiClient] Apiiro access token not configured. Please set apiiro.accessToken in your app-config."
|
|
105
|
+
);
|
|
106
|
+
return {
|
|
107
|
+
items: [],
|
|
108
|
+
totalCount: 0
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const page = await this.fetchApplicationsPage(nextCursor ?? void 0);
|
|
112
|
+
items.push(...page.items);
|
|
113
|
+
nextCursor = page.next;
|
|
114
|
+
} while (nextCursor);
|
|
115
|
+
return { items, totalCount: items.length };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
exports.ApiiroApiClient = ApiiroApiClient;
|
|
120
|
+
//# sourceMappingURL=apiClient.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiClient.cjs.js","sources":["../../src/helpers/apiClient.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport fetch from 'node-fetch';\nimport { APIIRO_DEFAULT_BASE_URL } from '@backstage-community/plugin-apiiro-common';\nimport {\n ApiiroRepositoriesResponse,\n ApiiroApplicationsResponse,\n RepositoryItem,\n ApplicationItem,\n PAGE_LIMIT,\n MAX_PAGES,\n} from './types';\n\nexport class ApiiroApiClient {\n constructor(private readonly accessToken: string | undefined) {}\n\n private async fetchRepositoriesPage(\n pageCursor?: string,\n ): Promise<ApiiroRepositoriesResponse> {\n const baseUrl = APIIRO_DEFAULT_BASE_URL;\n\n const params = new URLSearchParams();\n params.append('pageSize', PAGE_LIMIT.toString());\n if (pageCursor) {\n params.append('next', pageCursor);\n }\n\n const url = `${baseUrl}/rest-api/v2/repositories?${params.toString()}`;\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const errorMessage = `Failed to fetch repositories from Apiiro API. Status: ${response.status} ${response.statusText}`;\n throw new Error(errorMessage);\n }\n\n const data = (await response.json()) as ApiiroRepositoriesResponse;\n return {\n items: data.items || [],\n next: data.next || null,\n };\n }\n\n async fetchAllRepositories(): Promise<{\n items: RepositoryItem[];\n totalCount: number;\n }> {\n const items: RepositoryItem[] = [];\n let nextCursor: string | null | undefined = undefined;\n let pageCount = 0;\n\n do {\n pageCount++;\n\n if (pageCount > MAX_PAGES) {\n throw new Error(\n `Pagination limit exceeded: Maximum of ${MAX_PAGES} pages allowed. ` +\n `This may indicate an infinite loop or an unexpectedly large dataset. ` +\n `Fetched ${items.length} repositories so far.`,\n );\n }\n\n if (!this.accessToken) {\n console.warn(\n '[ApiiroApiClient] Apiiro access token not configured. Please set apiiro.accessToken in your app-config.',\n );\n return {\n items: [],\n totalCount: 0,\n };\n }\n\n const page = await this.fetchRepositoriesPage(nextCursor ?? undefined);\n items.push(...page.items);\n nextCursor = page.next;\n } while (nextCursor);\n\n return { items, totalCount: items.length };\n }\n\n private async fetchApplicationsPage(\n pageCursor?: string,\n ): Promise<ApiiroApplicationsResponse> {\n const baseUrl = APIIRO_DEFAULT_BASE_URL;\n\n const params = new URLSearchParams();\n params.append('pageSize', PAGE_LIMIT.toString());\n if (pageCursor) {\n params.append('next', pageCursor);\n }\n\n const url = `${baseUrl}/rest-api/v1/applications/profiles?${params.toString()}`;\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const errorMessage = `Failed to fetch applications from Apiiro API. Status: ${response.status} ${response.statusText}`;\n throw new Error(errorMessage);\n }\n\n const data = (await response.json()) as ApiiroApplicationsResponse;\n return {\n items: data.items || [],\n next: data.next || null,\n };\n }\n\n async fetchAllApplications(): Promise<{\n items: ApplicationItem[];\n totalCount: number;\n }> {\n const items: ApplicationItem[] = [];\n let nextCursor: string | null | undefined = undefined;\n let pageCount = 0;\n\n do {\n pageCount++;\n\n if (pageCount > MAX_PAGES) {\n throw new Error(\n `Pagination limit exceeded: Maximum of ${MAX_PAGES} pages allowed. ` +\n `This may indicate an infinite loop or an unexpectedly large dataset. ` +\n `Fetched ${items.length} applications so far.`,\n );\n }\n\n if (!this.accessToken) {\n console.warn(\n '[ApiiroApiClient] Apiiro access token not configured. Please set apiiro.accessToken in your app-config.',\n );\n return {\n items: [],\n totalCount: 0,\n };\n }\n const page = await this.fetchApplicationsPage(nextCursor ?? undefined);\n items.push(...page.items);\n nextCursor = page.next;\n } while (nextCursor);\n return { items, totalCount: items.length };\n }\n}\n"],"names":["APIIRO_DEFAULT_BASE_URL","PAGE_LIMIT","fetch","MAX_PAGES"],"mappings":"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"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
4
|
+
var types = require('./types.cjs.js');
|
|
5
|
+
|
|
6
|
+
class CacheManager {
|
|
7
|
+
constructor(apiClient, cacheService, catalogApi, auth) {
|
|
8
|
+
this.apiClient = apiClient;
|
|
9
|
+
this.cacheService = cacheService;
|
|
10
|
+
this.catalogApi = catalogApi;
|
|
11
|
+
this.auth = auth;
|
|
12
|
+
}
|
|
13
|
+
// Cache keys for Backstage CacheService
|
|
14
|
+
static REPO_CACHE_KEY = "apiiro:repositories";
|
|
15
|
+
static APP_CACHE_KEY = "apiiro:applications";
|
|
16
|
+
static ENTITY_REF_CACHE_KEY = "apiiro:entity-refs";
|
|
17
|
+
static REPO_LOCK_KEY = "apiiro:repositories:lock";
|
|
18
|
+
static APP_LOCK_KEY = "apiiro:applications:lock";
|
|
19
|
+
static ENTITY_REF_LOCK_KEY = "apiiro:entity-refs:lock";
|
|
20
|
+
isCacheExpired(lastFetched) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
return now - lastFetched > types.CACHE_TTL_MS;
|
|
23
|
+
}
|
|
24
|
+
buildRepositoryUrlToKeyMap(repositories) {
|
|
25
|
+
const urlToKeyMap = {};
|
|
26
|
+
const tempMap = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const repo of repositories) {
|
|
28
|
+
if (!repo.url || !repo.key) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const existing = tempMap.get(repo.url);
|
|
32
|
+
if (!existing || repo.isDefaultBranch && !existing.isDefaultBranch) {
|
|
33
|
+
tempMap.set(repo.url, {
|
|
34
|
+
key: repo.key,
|
|
35
|
+
isDefaultBranch: repo.isDefaultBranch || false
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
tempMap.forEach((value, key) => {
|
|
40
|
+
urlToKeyMap[key] = value.key;
|
|
41
|
+
});
|
|
42
|
+
return urlToKeyMap;
|
|
43
|
+
}
|
|
44
|
+
async fetchAndBuildRepoMap() {
|
|
45
|
+
try {
|
|
46
|
+
const { items } = await this.apiClient.fetchAllRepositories();
|
|
47
|
+
return this.buildRepositoryUrlToKeyMap(items);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
50
|
+
console.error(
|
|
51
|
+
`[CacheManager] Error fetching repositories from Apiiro API: ${errorMessage}`
|
|
52
|
+
);
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async refreshRepositoryCacheIfNeeded() {
|
|
57
|
+
const cachedData = await this.cacheService.get(
|
|
58
|
+
CacheManager.REPO_CACHE_KEY
|
|
59
|
+
);
|
|
60
|
+
if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const refreshLock = await this.cacheService.get(CacheManager.REPO_LOCK_KEY);
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
if (refreshLock && now - refreshLock.timestamp < types.REFRESH_LOCK_TTL_MS) {
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
67
|
+
await this.refreshRepositoryCacheIfNeeded();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await this.cacheService.set(CacheManager.REPO_LOCK_KEY, {
|
|
71
|
+
locked: true,
|
|
72
|
+
timestamp: now
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
const urlToKeyMap = await this.fetchAndBuildRepoMap();
|
|
76
|
+
await this.cacheService.set(
|
|
77
|
+
CacheManager.REPO_CACHE_KEY,
|
|
78
|
+
{
|
|
79
|
+
urlToKeyMap,
|
|
80
|
+
lastFetched: Date.now(),
|
|
81
|
+
fetchCompleted: true
|
|
82
|
+
},
|
|
83
|
+
{ ttl: types.CACHE_TTL_MS }
|
|
84
|
+
);
|
|
85
|
+
await this.cacheService.delete(CacheManager.REPO_LOCK_KEY);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
await this.cacheService.delete(CacheManager.REPO_LOCK_KEY);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async getRepoKey(repoUrl) {
|
|
92
|
+
if (!repoUrl) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
await this.refreshRepositoryCacheIfNeeded();
|
|
96
|
+
const cachedData = await this.cacheService.get(
|
|
97
|
+
CacheManager.REPO_CACHE_KEY
|
|
98
|
+
);
|
|
99
|
+
return cachedData?.urlToKeyMap[repoUrl] || null;
|
|
100
|
+
}
|
|
101
|
+
buildApplicationUidToKeyMap(applications) {
|
|
102
|
+
const uidToKeyMap = {};
|
|
103
|
+
for (const app of applications) {
|
|
104
|
+
if (!app.key || !app.externalSources || app.externalSources.length === 0) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const source of app.externalSources) {
|
|
108
|
+
if (source.server.provider === "Backstage") {
|
|
109
|
+
uidToKeyMap[source.id] = app.key;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return uidToKeyMap;
|
|
114
|
+
}
|
|
115
|
+
async fetchAndBuildAppMap() {
|
|
116
|
+
try {
|
|
117
|
+
const { items } = await this.apiClient.fetchAllApplications();
|
|
118
|
+
return this.buildApplicationUidToKeyMap(items);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
121
|
+
console.error(
|
|
122
|
+
`[CacheManager] Error fetching applications from Apiiro API: ${errorMessage}`
|
|
123
|
+
);
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async refreshApplicationCacheIfNeeded() {
|
|
128
|
+
const cachedData = await this.cacheService.get(
|
|
129
|
+
CacheManager.APP_CACHE_KEY
|
|
130
|
+
);
|
|
131
|
+
if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const refreshLock = await this.cacheService.get(CacheManager.APP_LOCK_KEY);
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
if (refreshLock && now - refreshLock.timestamp < types.REFRESH_LOCK_TTL_MS) {
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
138
|
+
await this.refreshApplicationCacheIfNeeded();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
await this.cacheService.set(CacheManager.APP_LOCK_KEY, {
|
|
142
|
+
locked: true,
|
|
143
|
+
timestamp: now
|
|
144
|
+
});
|
|
145
|
+
try {
|
|
146
|
+
const uidToKeyMap = await this.fetchAndBuildAppMap();
|
|
147
|
+
await this.cacheService.set(
|
|
148
|
+
CacheManager.APP_CACHE_KEY,
|
|
149
|
+
{
|
|
150
|
+
uidToKeyMap,
|
|
151
|
+
lastFetched: Date.now(),
|
|
152
|
+
fetchCompleted: true
|
|
153
|
+
},
|
|
154
|
+
{ ttl: types.CACHE_TTL_MS }
|
|
155
|
+
);
|
|
156
|
+
await this.cacheService.delete(CacheManager.APP_LOCK_KEY);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
await this.cacheService.delete(CacheManager.APP_LOCK_KEY);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async getApplicationId(entityUid) {
|
|
163
|
+
if (!entityUid) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
await this.refreshApplicationCacheIfNeeded();
|
|
167
|
+
const cachedData = await this.cacheService.get(
|
|
168
|
+
CacheManager.APP_CACHE_KEY
|
|
169
|
+
);
|
|
170
|
+
return cachedData?.uidToKeyMap[entityUid] || null;
|
|
171
|
+
}
|
|
172
|
+
async fetchAndBuildEntityRefMap() {
|
|
173
|
+
if (!this.catalogApi || !this.auth) {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const { token } = await this.auth.getPluginRequestToken({
|
|
178
|
+
onBehalfOf: await this.auth.getOwnServiceCredentials(),
|
|
179
|
+
targetPluginId: "catalog"
|
|
180
|
+
});
|
|
181
|
+
const { items } = await this.catalogApi.getEntities(
|
|
182
|
+
{
|
|
183
|
+
filter: { kind: "System" },
|
|
184
|
+
fields: [
|
|
185
|
+
"kind",
|
|
186
|
+
"metadata.uid",
|
|
187
|
+
"metadata.name",
|
|
188
|
+
"metadata.namespace"
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
{ token }
|
|
192
|
+
);
|
|
193
|
+
const refToUidMap = {};
|
|
194
|
+
for (const entity of items) {
|
|
195
|
+
if (entity.metadata.uid) {
|
|
196
|
+
const entityRef = catalogModel.stringifyEntityRef({
|
|
197
|
+
kind: entity.kind,
|
|
198
|
+
name: entity.metadata.name,
|
|
199
|
+
namespace: entity.metadata.namespace
|
|
200
|
+
});
|
|
201
|
+
refToUidMap[entityRef] = entity.metadata.uid;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return refToUidMap;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
207
|
+
console.error(
|
|
208
|
+
`[CacheManager] Error fetching entities from catalog: ${errorMessage}`
|
|
209
|
+
);
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async refreshEntityRefCacheIfNeeded() {
|
|
214
|
+
const cachedData = await this.cacheService.get(
|
|
215
|
+
CacheManager.ENTITY_REF_CACHE_KEY
|
|
216
|
+
);
|
|
217
|
+
if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const refreshLock = await this.cacheService.get(CacheManager.ENTITY_REF_LOCK_KEY);
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
if (refreshLock && now - refreshLock.timestamp < types.REFRESH_LOCK_TTL_MS) {
|
|
223
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
224
|
+
await this.refreshEntityRefCacheIfNeeded();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await this.cacheService.set(CacheManager.ENTITY_REF_LOCK_KEY, {
|
|
228
|
+
locked: true,
|
|
229
|
+
timestamp: now
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
const refToUidMap = await this.fetchAndBuildEntityRefMap();
|
|
233
|
+
await this.cacheService.set(
|
|
234
|
+
CacheManager.ENTITY_REF_CACHE_KEY,
|
|
235
|
+
{
|
|
236
|
+
refToUidMap,
|
|
237
|
+
lastFetched: Date.now(),
|
|
238
|
+
fetchCompleted: true
|
|
239
|
+
},
|
|
240
|
+
{ ttl: types.CACHE_TTL_MS }
|
|
241
|
+
);
|
|
242
|
+
await this.cacheService.delete(CacheManager.ENTITY_REF_LOCK_KEY);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
await this.cacheService.delete(CacheManager.ENTITY_REF_LOCK_KEY);
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async invalidateEntityRefCache() {
|
|
249
|
+
await this.cacheService.set(CacheManager.ENTITY_REF_CACHE_KEY, {
|
|
250
|
+
refToUidMap: {},
|
|
251
|
+
lastFetched: 0,
|
|
252
|
+
fetchCompleted: false
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async getEntityUid(entityRef) {
|
|
256
|
+
if (!entityRef) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
await this.refreshEntityRefCacheIfNeeded();
|
|
260
|
+
const cachedData = await this.cacheService.get(
|
|
261
|
+
CacheManager.ENTITY_REF_CACHE_KEY
|
|
262
|
+
);
|
|
263
|
+
return cachedData?.refToUidMap[entityRef] || null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
exports.CacheManager = CacheManager;
|
|
268
|
+
//# sourceMappingURL=cacheManager.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cacheManager.cjs.js","sources":["../../src/helpers/cacheManager.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { CatalogApi } from '@backstage/catalog-client';\nimport { AuthService, CacheService } from '@backstage/backend-plugin-api';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { ApiiroApiClient } from './apiClient';\nimport {\n RepositoryItem,\n ApplicationItem,\n CachedRepositoryData,\n CachedApplicationData,\n CachedEntityRefData,\n CACHE_TTL_MS,\n REFRESH_LOCK_TTL_MS,\n} from './types';\n\nexport class CacheManager {\n // Cache keys for Backstage CacheService\n private static readonly REPO_CACHE_KEY = 'apiiro:repositories';\n private static readonly APP_CACHE_KEY = 'apiiro:applications';\n private static readonly ENTITY_REF_CACHE_KEY = 'apiiro:entity-refs';\n private static readonly REPO_LOCK_KEY = 'apiiro:repositories:lock';\n private static readonly APP_LOCK_KEY = 'apiiro:applications:lock';\n private static readonly ENTITY_REF_LOCK_KEY = 'apiiro:entity-refs:lock';\n\n constructor(\n private readonly apiClient: ApiiroApiClient,\n private readonly cacheService: CacheService,\n private readonly catalogApi?: CatalogApi,\n private readonly auth?: AuthService,\n ) {}\n\n private isCacheExpired(lastFetched: number): boolean {\n const now = Date.now();\n return now - lastFetched > CACHE_TTL_MS;\n }\n\n private buildRepositoryUrlToKeyMap(\n repositories: RepositoryItem[],\n ): Record<string, string> {\n const urlToKeyMap: Record<string, string> = {};\n const tempMap = new Map<\n string,\n { key: string; isDefaultBranch: boolean }\n >();\n\n for (const repo of repositories) {\n if (!repo.url || !repo.key) {\n continue;\n }\n\n const existing = tempMap.get(repo.url);\n if (!existing || (repo.isDefaultBranch && !existing.isDefaultBranch)) {\n tempMap.set(repo.url, {\n key: repo.key,\n isDefaultBranch: repo.isDefaultBranch || false,\n });\n }\n }\n\n tempMap.forEach((value, key) => {\n urlToKeyMap[key] = value.key;\n });\n\n return urlToKeyMap;\n }\n\n private async fetchAndBuildRepoMap(): Promise<Record<string, string>> {\n try {\n const { items } = await this.apiClient.fetchAllRepositories();\n return this.buildRepositoryUrlToKeyMap(items);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n console.error(\n `[CacheManager] Error fetching repositories from Apiiro API: ${errorMessage}`,\n );\n return {};\n }\n }\n\n async refreshRepositoryCacheIfNeeded(): Promise<void> {\n const cachedData = await this.cacheService.get<CachedRepositoryData>(\n CacheManager.REPO_CACHE_KEY,\n );\n\n if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {\n return;\n }\n\n const refreshLock = await this.cacheService.get<{\n locked: boolean;\n timestamp: number;\n }>(CacheManager.REPO_LOCK_KEY);\n const now = Date.now();\n if (refreshLock && now - refreshLock.timestamp < REFRESH_LOCK_TTL_MS) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n await this.refreshRepositoryCacheIfNeeded();\n return;\n }\n\n await this.cacheService.set(CacheManager.REPO_LOCK_KEY, {\n locked: true,\n timestamp: now,\n });\n\n try {\n const urlToKeyMap = await this.fetchAndBuildRepoMap();\n\n await this.cacheService.set(\n CacheManager.REPO_CACHE_KEY,\n {\n urlToKeyMap,\n lastFetched: Date.now(),\n fetchCompleted: true,\n },\n { ttl: CACHE_TTL_MS },\n );\n\n await this.cacheService.delete(CacheManager.REPO_LOCK_KEY);\n } catch (error) {\n await this.cacheService.delete(CacheManager.REPO_LOCK_KEY);\n throw error;\n }\n }\n\n async getRepoKey(repoUrl: string): Promise<string | null> {\n if (!repoUrl) {\n return null;\n }\n\n await this.refreshRepositoryCacheIfNeeded();\n const cachedData = await this.cacheService.get<CachedRepositoryData>(\n CacheManager.REPO_CACHE_KEY,\n );\n return cachedData?.urlToKeyMap[repoUrl] || null;\n }\n\n private buildApplicationUidToKeyMap(\n applications: ApplicationItem[],\n ): Record<string, string> {\n const uidToKeyMap: Record<string, string> = {};\n\n for (const app of applications) {\n if (\n !app.key ||\n !app.externalSources ||\n app.externalSources.length === 0\n ) {\n continue;\n }\n\n for (const source of app.externalSources) {\n if (source.server.provider === 'Backstage') {\n uidToKeyMap[source.id] = app.key;\n }\n }\n }\n return uidToKeyMap;\n }\n\n private async fetchAndBuildAppMap(): Promise<Record<string, string>> {\n try {\n const { items } = await this.apiClient.fetchAllApplications();\n return this.buildApplicationUidToKeyMap(items);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n console.error(\n `[CacheManager] Error fetching applications from Apiiro API: ${errorMessage}`,\n );\n return {};\n }\n }\n\n async refreshApplicationCacheIfNeeded(): Promise<void> {\n const cachedData = await this.cacheService.get<CachedApplicationData>(\n CacheManager.APP_CACHE_KEY,\n );\n\n if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {\n return;\n }\n\n const refreshLock = await this.cacheService.get<{\n locked: boolean;\n timestamp: number;\n }>(CacheManager.APP_LOCK_KEY);\n const now = Date.now();\n if (refreshLock && now - refreshLock.timestamp < REFRESH_LOCK_TTL_MS) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n await this.refreshApplicationCacheIfNeeded();\n return;\n }\n\n await this.cacheService.set(CacheManager.APP_LOCK_KEY, {\n locked: true,\n timestamp: now,\n });\n\n try {\n const uidToKeyMap = await this.fetchAndBuildAppMap();\n await this.cacheService.set(\n CacheManager.APP_CACHE_KEY,\n {\n uidToKeyMap,\n lastFetched: Date.now(),\n fetchCompleted: true,\n },\n { ttl: CACHE_TTL_MS },\n );\n\n await this.cacheService.delete(CacheManager.APP_LOCK_KEY);\n } catch (error) {\n await this.cacheService.delete(CacheManager.APP_LOCK_KEY);\n throw error;\n }\n }\n\n async getApplicationId(\n entityUid: string | undefined,\n ): Promise<string | null> {\n if (!entityUid) {\n return null;\n }\n\n await this.refreshApplicationCacheIfNeeded();\n const cachedData = await this.cacheService.get<CachedApplicationData>(\n CacheManager.APP_CACHE_KEY,\n );\n return cachedData?.uidToKeyMap[entityUid] || null;\n }\n\n private async fetchAndBuildEntityRefMap(): Promise<Record<string, string>> {\n if (!this.catalogApi || !this.auth) {\n return {};\n }\n\n try {\n const { token } = await this.auth.getPluginRequestToken({\n onBehalfOf: await this.auth.getOwnServiceCredentials(),\n targetPluginId: 'catalog',\n });\n\n const { items } = await this.catalogApi.getEntities(\n {\n filter: { kind: 'System' },\n fields: [\n 'kind',\n 'metadata.uid',\n 'metadata.name',\n 'metadata.namespace',\n ],\n },\n { token },\n );\n\n const refToUidMap: Record<string, string> = {};\n for (const entity of items) {\n if (entity.metadata.uid) {\n const entityRef = stringifyEntityRef({\n kind: entity.kind,\n name: entity.metadata.name,\n namespace: entity.metadata.namespace,\n });\n refToUidMap[entityRef] = entity.metadata.uid;\n }\n }\n\n return refToUidMap;\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n console.error(\n `[CacheManager] Error fetching entities from catalog: ${errorMessage}`,\n );\n return {};\n }\n }\n\n async refreshEntityRefCacheIfNeeded(): Promise<void> {\n const cachedData = await this.cacheService.get<CachedEntityRefData>(\n CacheManager.ENTITY_REF_CACHE_KEY,\n );\n\n if (cachedData && !this.isCacheExpired(cachedData.lastFetched)) {\n return;\n }\n\n const refreshLock = await this.cacheService.get<{\n locked: boolean;\n timestamp: number;\n }>(CacheManager.ENTITY_REF_LOCK_KEY);\n const now = Date.now();\n if (refreshLock && now - refreshLock.timestamp < REFRESH_LOCK_TTL_MS) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n await this.refreshEntityRefCacheIfNeeded();\n return;\n }\n\n await this.cacheService.set(CacheManager.ENTITY_REF_LOCK_KEY, {\n locked: true,\n timestamp: now,\n });\n\n try {\n const refToUidMap = await this.fetchAndBuildEntityRefMap();\n await this.cacheService.set(\n CacheManager.ENTITY_REF_CACHE_KEY,\n {\n refToUidMap,\n lastFetched: Date.now(),\n fetchCompleted: true,\n },\n { ttl: CACHE_TTL_MS },\n );\n\n await this.cacheService.delete(CacheManager.ENTITY_REF_LOCK_KEY);\n } catch (error) {\n await this.cacheService.delete(CacheManager.ENTITY_REF_LOCK_KEY);\n throw error;\n }\n }\n\n async invalidateEntityRefCache(): Promise<void> {\n await this.cacheService.set(CacheManager.ENTITY_REF_CACHE_KEY, {\n refToUidMap: {},\n lastFetched: 0,\n fetchCompleted: false,\n });\n }\n\n async getEntityUid(entityRef: string): Promise<string | null> {\n if (!entityRef) {\n return null;\n }\n\n await this.refreshEntityRefCacheIfNeeded();\n const cachedData = await this.cacheService.get<CachedEntityRefData>(\n CacheManager.ENTITY_REF_CACHE_KEY,\n );\n\n return cachedData?.refToUidMap[entityRef] || null;\n }\n}\n"],"names":["CACHE_TTL_MS","REFRESH_LOCK_TTL_MS","stringifyEntityRef"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const BACKSTAGE_SOURCE_LOCATION_ANNOTATION = "backstage.io/source-location";
|
|
4
|
+
const CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
5
|
+
const REFRESH_LOCK_TTL_MS = 2 * 60 * 1e3;
|
|
6
|
+
const PAGE_LIMIT = 1e3;
|
|
7
|
+
const MAX_PAGES = 1e3;
|
|
8
|
+
const AZURE_HOST_NAME = "dev.azure.com";
|
|
9
|
+
|
|
10
|
+
exports.AZURE_HOST_NAME = AZURE_HOST_NAME;
|
|
11
|
+
exports.BACKSTAGE_SOURCE_LOCATION_ANNOTATION = BACKSTAGE_SOURCE_LOCATION_ANNOTATION;
|
|
12
|
+
exports.CACHE_TTL_MS = CACHE_TTL_MS;
|
|
13
|
+
exports.MAX_PAGES = MAX_PAGES;
|
|
14
|
+
exports.PAGE_LIMIT = PAGE_LIMIT;
|
|
15
|
+
exports.REFRESH_LOCK_TTL_MS = REFRESH_LOCK_TTL_MS;
|
|
16
|
+
//# sourceMappingURL=types.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.cjs.js","sources":["../../src/helpers/types.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nexport interface RepositoryItem {\n name?: string | null;\n key?: string | null;\n url?: string | null;\n isDefaultBranch?: boolean;\n branchName?: string | null;\n [key: string]: unknown;\n}\n\nexport interface ApiiroRepositoriesResponse {\n items: RepositoryItem[];\n next?: string | null;\n}\n\nexport interface ExternalSource {\n id: string;\n server: {\n id: string;\n provider: string;\n tokenExpirationDate: string | null;\n url: string;\n };\n}\n\nexport interface ApplicationItem {\n name?: string | null;\n key?: string | null;\n externalSources?: ExternalSource[];\n [key: string]: unknown;\n}\n\nexport interface ApiiroApplicationsResponse {\n items: ApplicationItem[];\n next?: string | null;\n}\n\nexport interface CachedRepositoryData {\n urlToKeyMap: Record<string, string>;\n lastFetched: number;\n fetchCompleted: boolean;\n [key: string]: any;\n}\n\nexport interface CachedApplicationData {\n uidToKeyMap: Record<string, string>;\n lastFetched: number;\n fetchCompleted: boolean;\n [key: string]: any;\n}\n\nexport interface CachedEntityRefData {\n refToUidMap: Record<string, string>;\n lastFetched: number;\n fetchCompleted: boolean;\n [key: string]: any;\n}\n\nexport const BACKSTAGE_SOURCE_LOCATION_ANNOTATION =\n 'backstage.io/source-location';\n\nexport const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour\nexport const REFRESH_LOCK_TTL_MS = 2 * 60 * 1000; // 2 minutes\nexport const PAGE_LIMIT = 1000;\nexport const MAX_PAGES = 1000;\nexport const AZURE_HOST_NAME = 'dev.azure.com';\n"],"names":[],"mappings":";;AAwEO,MAAM,oCAAA,GACX;AAEK,MAAM,YAAA,GAAe,KAAK,EAAA,GAAK;AAC/B,MAAM,mBAAA,GAAsB,IAAI,EAAA,GAAK;AACrC,MAAM,UAAA,GAAa;AACnB,MAAM,SAAA,GAAY;AAClB,MAAM,eAAA,GAAkB;;;;;;;;;"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var types = require('./types.cjs.js');
|
|
4
|
+
var pathToRegexp = require('path-to-regexp');
|
|
5
|
+
|
|
6
|
+
function extractRepoUrlFromSourceLocation(sourceLocation) {
|
|
7
|
+
try {
|
|
8
|
+
if (!sourceLocation) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const cleanUrl = sourceLocation.replace(/^url:/, "");
|
|
12
|
+
const matches = cleanUrl.match(
|
|
13
|
+
/https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(:[0-9]{1,5})?(\/.*)?/g
|
|
14
|
+
);
|
|
15
|
+
if (!matches) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const url = new URL(matches[0]);
|
|
19
|
+
const hostname = url.host.toLowerCase();
|
|
20
|
+
let repoPath = null;
|
|
21
|
+
if (hostname === types.AZURE_HOST_NAME) {
|
|
22
|
+
const matcher = pathToRegexp.match("/:org/:project/_git/:repo", { end: false });
|
|
23
|
+
const extracted = matcher(url.pathname);
|
|
24
|
+
if (extracted) {
|
|
25
|
+
repoPath = `/${extracted.params.org}/${extracted.params.project}/_git/${extracted.params.repo}`;
|
|
26
|
+
}
|
|
27
|
+
} else if (hostname.includes("gitlab")) {
|
|
28
|
+
const cleanPath = url.pathname.replace(/\/-\/(tree|blob)\/[^\/]+.*$/, "");
|
|
29
|
+
const matcher = pathToRegexp.match("/:org/:repo", { end: false });
|
|
30
|
+
const extracted = matcher(cleanPath);
|
|
31
|
+
if (extracted) {
|
|
32
|
+
repoPath = cleanPath;
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
const matcher = pathToRegexp.match("/:org/:repo", { end: false });
|
|
36
|
+
const extracted = matcher(url.pathname);
|
|
37
|
+
if (extracted) {
|
|
38
|
+
repoPath = `/${extracted.params.org}/${extracted.params.repo}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return repoPath ? `${url.protocol}//${hostname}${repoPath}` : null;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
exports.extractRepoUrlFromSourceLocation = extractRepoUrlFromSourceLocation;
|
|
48
|
+
//# sourceMappingURL=utils.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.cjs.js","sources":["../../src/helpers/utils.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { AZURE_HOST_NAME } from './types';\nimport { match } from 'path-to-regexp';\n\nexport function extractRepoUrlFromSourceLocation(\n sourceLocation?: string,\n): string | null {\n try {\n if (!sourceLocation) {\n return null;\n }\n\n // Extract the base URL (remove \"url:\" prefix if present)\n const cleanUrl = sourceLocation.replace(/^url:/, '');\n const matches = cleanUrl.match(\n /https?:\\/\\/[a-zA-Z0-9\\-\\.]+\\.[a-zA-Z]{2,}(:[0-9]{1,5})?(\\/.*)?/g,\n );\n\n if (!matches) {\n return null;\n }\n const url = new URL(matches[0]);\n const hostname = url.host.toLowerCase();\n\n // Handle different SCM providers using path-to-regexp patterns\n let repoPath: string | null = null;\n if (hostname === AZURE_HOST_NAME) {\n // Azure DevOps format: /org/project/_git/repo\n const matcher = match('/:org/:project/_git/:repo', { end: false });\n const extracted = matcher(url.pathname);\n if (extracted) {\n repoPath = `/${extracted.params.org}/${extracted.params.project}/_git/${extracted.params.repo}`;\n }\n } else if (hostname.includes('gitlab')) {\n // GitLab format: /org/repo or /group/subgroup/.../repo\n // Remove GitLab-specific parts like /-/tree/branch\n const cleanPath = url.pathname.replace(/\\/-\\/(tree|blob)\\/[^\\/]+.*$/, '');\n\n // Use a pattern that matches any number of segments between org and repo\n const matcher = match('/:org/:repo', { end: false });\n const extracted = matcher(cleanPath);\n\n if (extracted) {\n // For GitLab, preserve the full path structure to support subgroups\n repoPath = cleanPath;\n }\n } else {\n // GitHub and Bitbucket format: /org/repo\n const matcher = match('/:org/:repo', { end: false });\n const extracted = matcher(url.pathname);\n if (extracted) {\n repoPath = `/${extracted.params.org}/${extracted.params.repo}`;\n }\n }\n\n return repoPath ? `${url.protocol}//${hostname}${repoPath}` : null;\n } catch (error) {\n return null;\n }\n}\n"],"names":["AZURE_HOST_NAME","match"],"mappings":";;;;;AAkBO,SAAS,iCACd,cAAA,EACe;AACf,EAAA,IAAI;AACF,IAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,MAAM,QAAA,GAAW,cAAA,CAAe,OAAA,CAAQ,OAAA,EAAS,EAAE,CAAA;AACnD,IAAA,MAAM,UAAU,QAAA,CAAS,KAAA;AAAA,MACvB;AAAA,KACF;AAEA,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,CAAC,CAAC,CAAA;AAC9B,IAAA,MAAM,QAAA,GAAW,GAAA,CAAI,IAAA,CAAK,WAAA,EAAY;AAGtC,IAAA,IAAI,QAAA,GAA0B,IAAA;AAC9B,IAAA,IAAI,aAAaA,qBAAA,EAAiB;AAEhC,MAAA,MAAM,UAAUC,kBAAA,CAAM,2BAAA,EAA6B,EAAE,GAAA,EAAK,OAAO,CAAA;AACjE,MAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AACtC,MAAA,IAAI,SAAA,EAAW;AACb,QAAA,QAAA,GAAW,CAAA,CAAA,EAAI,SAAA,CAAU,MAAA,CAAO,GAAG,CAAA,CAAA,EAAI,SAAA,CAAU,MAAA,CAAO,OAAO,CAAA,MAAA,EAAS,SAAA,CAAU,MAAA,CAAO,IAAI,CAAA,CAAA;AAAA,MAC/F;AAAA,IACF,CAAA,MAAA,IAAW,QAAA,CAAS,QAAA,CAAS,QAAQ,CAAA,EAAG;AAGtC,MAAA,MAAM,SAAA,GAAY,GAAA,CAAI,QAAA,CAAS,OAAA,CAAQ,+BAA+B,EAAE,CAAA;AAGxE,MAAA,MAAM,UAAUA,kBAAA,CAAM,aAAA,EAAe,EAAE,GAAA,EAAK,OAAO,CAAA;AACnD,MAAA,MAAM,SAAA,GAAY,QAAQ,SAAS,CAAA;AAEnC,MAAA,IAAI,SAAA,EAAW;AAEb,QAAA,QAAA,GAAW,SAAA;AAAA,MACb;AAAA,IACF,CAAA,MAAO;AAEL,MAAA,MAAM,UAAUA,kBAAA,CAAM,aAAA,EAAe,EAAE,GAAA,EAAK,OAAO,CAAA;AACnD,MAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AACtC,MAAA,IAAI,SAAA,EAAW;AACb,QAAA,QAAA,GAAW,IAAI,SAAA,CAAU,MAAA,CAAO,GAAG,CAAA,CAAA,EAAI,SAAA,CAAU,OAAO,IAAI,CAAA,CAAA;AAAA,MAC9D;AAAA,IACF;AAEA,IAAA,OAAO,QAAA,GAAW,GAAG,GAAA,CAAI,QAAQ,KAAK,QAAQ,CAAA,EAAG,QAAQ,CAAA,CAAA,GAAK,IAAA;AAAA,EAChE,SAAS,KAAA,EAAO;AACd,IAAA,OAAO,IAAA;AAAA,EACT;AACF;;;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
+
* Catalog backend module for Apiiro entity annotation processing.
|
|
5
|
+
*
|
|
6
|
+
* Automatically adds Apiiro annotations to Component and System entities
|
|
7
|
+
* based on their source location or name, enabling integration with Apiiro security insights.
|
|
8
|
+
*
|
|
4
9
|
* @public
|
|
5
10
|
*/
|
|
6
11
|
declare const catalogModuleApiiroEntityProcessor: _backstage_backend_plugin_api.BackendFeature;
|
package/dist/module.cjs.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
-
var
|
|
4
|
+
var pluginCatalogNode = require('@backstage/plugin-catalog-node');
|
|
5
|
+
var catalogClient = require('@backstage/catalog-client');
|
|
5
6
|
var ApiiroAnnotationProcessor = require('./processor/ApiiroAnnotationProcessor.cjs.js');
|
|
6
7
|
|
|
7
8
|
const catalogModuleApiiroEntityProcessor = backendPluginApi.createBackendModule({
|
|
@@ -10,11 +11,17 @@ const catalogModuleApiiroEntityProcessor = backendPluginApi.createBackendModule(
|
|
|
10
11
|
register(reg) {
|
|
11
12
|
reg.registerInit({
|
|
12
13
|
deps: {
|
|
13
|
-
catalog:
|
|
14
|
-
config: backendPluginApi.coreServices.rootConfig
|
|
14
|
+
catalog: pluginCatalogNode.catalogProcessingExtensionPoint,
|
|
15
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
16
|
+
discovery: backendPluginApi.coreServices.discovery,
|
|
17
|
+
auth: backendPluginApi.coreServices.auth,
|
|
18
|
+
cache: backendPluginApi.coreServices.cache
|
|
15
19
|
},
|
|
16
|
-
async init({ catalog, config }) {
|
|
17
|
-
|
|
20
|
+
async init({ catalog, config, discovery, auth, cache }) {
|
|
21
|
+
const catalogApi = new catalogClient.CatalogClient({ discoveryApi: discovery });
|
|
22
|
+
catalog.addProcessor(
|
|
23
|
+
new ApiiroAnnotationProcessor.ApiiroAnnotationProcessor(config, { catalogApi, auth, cache })
|
|
24
|
+
);
|
|
18
25
|
}
|
|
19
26
|
});
|
|
20
27
|
}
|
package/dist/module.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.cjs.js","sources":["../src/module.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendModule,\n} from '@backstage/backend-plugin-api';\nimport { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/
|
|
1
|
+
{"version":3,"file":"module.cjs.js","sources":["../src/module.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendModule,\n} from '@backstage/backend-plugin-api';\nimport { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';\nimport { CatalogClient } from '@backstage/catalog-client';\nimport { ApiiroAnnotationProcessor } from './processor';\n\n/**\n * Catalog backend module for Apiiro entity annotation processing.\n *\n * Automatically adds Apiiro annotations to Component and System entities\n * based on their source location or name, enabling integration with Apiiro security insights.\n *\n * @public\n */\nexport const catalogModuleApiiroEntityProcessor = createBackendModule({\n pluginId: 'catalog',\n moduleId: 'apiiro-entity-processor',\n register(reg) {\n reg.registerInit({\n deps: {\n catalog: catalogProcessingExtensionPoint,\n config: coreServices.rootConfig,\n discovery: coreServices.discovery,\n auth: coreServices.auth,\n cache: coreServices.cache,\n },\n async init({ catalog, config, discovery, auth, cache }) {\n const catalogApi = new CatalogClient({ discoveryApi: discovery });\n catalog.addProcessor(\n new ApiiroAnnotationProcessor(config, { catalogApi, auth, cache }),\n );\n },\n });\n },\n});\n"],"names":["createBackendModule","catalogProcessingExtensionPoint","coreServices","CatalogClient","ApiiroAnnotationProcessor"],"mappings":";;;;;;;AAgCO,MAAM,qCAAqCA,oCAAA,CAAoB;AAAA,EACpE,QAAA,EAAU,SAAA;AAAA,EACV,QAAA,EAAU,yBAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,OAAA,EAASC,iDAAA;AAAA,QACT,QAAQC,6BAAA,CAAa,UAAA;AAAA,QACrB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,OAAOA,6BAAA,CAAa;AAAA,OACtB;AAAA,MACA,MAAM,KAAK,EAAE,OAAA,EAAS,QAAQ,SAAA,EAAW,IAAA,EAAM,OAAM,EAAG;AACtD,QAAA,MAAM,aAAa,IAAIC,2BAAA,CAAc,EAAE,YAAA,EAAc,WAAW,CAAA;AAChE,QAAA,OAAA,CAAQ,YAAA;AAAA,UACN,IAAIC,mDAAA,CAA0B,MAAA,EAAQ,EAAE,UAAA,EAAY,IAAA,EAAM,OAAO;AAAA,SACnE;AAAA,MACF;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
|
|
@@ -1,287 +1,100 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
4
4
|
var pluginApiiroCommon = require('@backstage-community/plugin-apiiro-common');
|
|
5
|
+
var apiClient = require('../helpers/apiClient.cjs.js');
|
|
6
|
+
var cacheManager = require('../helpers/cacheManager.cjs.js');
|
|
7
|
+
var utils = require('../helpers/utils.cjs.js');
|
|
8
|
+
var types = require('../helpers/types.cjs.js');
|
|
5
9
|
|
|
6
|
-
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
|
-
|
|
8
|
-
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
|
|
9
|
-
|
|
10
|
-
const BACKSTAGE_SOURCE_LOCATION_ANNOTATION = "backstage.io/source-location";
|
|
11
10
|
class ApiiroAnnotationProcessor {
|
|
12
|
-
constructor(config) {
|
|
11
|
+
constructor(config, options) {
|
|
13
12
|
this.config = config;
|
|
13
|
+
const accessToken = this.config.getOptionalString("apiiro.accessToken");
|
|
14
|
+
this.apiClient = new apiClient.ApiiroApiClient(accessToken);
|
|
15
|
+
this.catalogApi = options?.catalogApi;
|
|
16
|
+
this.auth = options?.auth;
|
|
17
|
+
this.cache = options.cache;
|
|
18
|
+
this.cacheManager = new cacheManager.CacheManager(
|
|
19
|
+
this.apiClient,
|
|
20
|
+
this.cache,
|
|
21
|
+
this.catalogApi,
|
|
22
|
+
this.auth
|
|
23
|
+
);
|
|
14
24
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
repoCache = {
|
|
21
|
-
data: /* @__PURE__ */ new Map(),
|
|
22
|
-
lastFetched: 0,
|
|
23
|
-
isRefreshing: false
|
|
24
|
-
};
|
|
25
|
+
apiClient;
|
|
26
|
+
cacheManager;
|
|
27
|
+
catalogApi;
|
|
28
|
+
auth;
|
|
29
|
+
cache;
|
|
25
30
|
getProcessorName() {
|
|
26
31
|
return "ApiiroAnnotationProcessor";
|
|
27
32
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Determines if an entity should be processed by this processor.
|
|
30
|
-
* Only Component entities are processed.
|
|
31
|
-
*/
|
|
32
33
|
shouldProcessEntity(entity) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
extractRepoUrlFromSourceLocation(sourceLocation) {
|
|
36
|
-
try {
|
|
37
|
-
if (!sourceLocation) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
const matches = sourceLocation.match(
|
|
41
|
-
/https?:\/\/[a-zA-Z0-9\-.]+\.[a-zA-Z]{2,}(:[0-9]{1,5})?(\/.*)?/g
|
|
42
|
-
);
|
|
43
|
-
if (!matches) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
const url = new URL(matches[0]);
|
|
47
|
-
const hostname = url.host.toLowerCase();
|
|
48
|
-
let repoPath = null;
|
|
49
|
-
if (hostname === "dev.azure.com") {
|
|
50
|
-
const pathMatch = url.pathname.match(
|
|
51
|
-
/^\/([^/]+)\/([^/]+)\/_git\/([^/]+)(?:\/.*)?$/
|
|
52
|
-
);
|
|
53
|
-
if (pathMatch) {
|
|
54
|
-
repoPath = `/${pathMatch[1]}/${pathMatch[2]}/_git/${pathMatch[3]}`;
|
|
55
|
-
}
|
|
56
|
-
} else if (hostname.includes("gitlab")) {
|
|
57
|
-
const cleanPath = url.pathname.replace(/\/-\/.*$/, "");
|
|
58
|
-
const pathMatch = cleanPath.match(/^(\/[^/]+\/[^/]+(?:\/[^/]+)*)$/);
|
|
59
|
-
if (pathMatch) {
|
|
60
|
-
repoPath = pathMatch[1];
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
const pathMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)(?:\/.*)?$/);
|
|
64
|
-
if (pathMatch) {
|
|
65
|
-
repoPath = `/${pathMatch[1]}/${pathMatch[2]}`;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (!repoPath) {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
return `${url.protocol}//${hostname}${repoPath}`;
|
|
72
|
-
} catch (error) {
|
|
73
|
-
return null;
|
|
34
|
+
if (entity.kind === "Component") {
|
|
35
|
+
return true;
|
|
74
36
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* Retrieves the Apiiro access token from configuration.
|
|
78
|
-
* @throws {Error} If the access token is not configured
|
|
79
|
-
*/
|
|
80
|
-
getAccessToken() {
|
|
81
|
-
const accessToken = this.config.getOptionalString("apiiro.accessToken");
|
|
82
|
-
return accessToken;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Fetches a single page of repositories from the Apiiro API.
|
|
86
|
-
* @param pageCursor - Optional cursor for pagination
|
|
87
|
-
* @returns Page of repositories with next cursor
|
|
88
|
-
*/
|
|
89
|
-
async fetchRepositoriesPage(accessToken, pageCursor) {
|
|
90
|
-
const baseUrl = pluginApiiroCommon.APIIRO_DEFAULT_BASE_URL;
|
|
91
|
-
const params = new URLSearchParams();
|
|
92
|
-
params.append("limit", ApiiroAnnotationProcessor.PAGE_LIMIT.toString());
|
|
93
|
-
if (pageCursor) {
|
|
94
|
-
params.append("next", pageCursor);
|
|
95
|
-
}
|
|
96
|
-
const url = `${baseUrl}/rest-api/v2/repositories?${params.toString()}`;
|
|
97
|
-
const response = await fetch__default.default(url, {
|
|
98
|
-
method: "GET",
|
|
99
|
-
headers: {
|
|
100
|
-
Authorization: `Bearer ${accessToken}`,
|
|
101
|
-
"Content-Type": "application/json"
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
if (!response.ok) {
|
|
105
|
-
const errorMessage = `Failed to fetch repositories from Apiiro API. Status: ${response.status} ${response.statusText}`;
|
|
106
|
-
throw new Error(errorMessage);
|
|
37
|
+
if (entity.kind === "System" && this.isApplicationsViewEnabled()) {
|
|
38
|
+
return true;
|
|
107
39
|
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
items: data.items || [],
|
|
111
|
-
next: data.next || null
|
|
112
|
-
};
|
|
40
|
+
return false;
|
|
113
41
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* @returns All repositories with total count
|
|
117
|
-
* @throws {Error} If pagination limit is exceeded
|
|
118
|
-
*/
|
|
119
|
-
async fetchAllRepositories() {
|
|
120
|
-
const items = [];
|
|
121
|
-
let nextCursor = void 0;
|
|
122
|
-
let pageCount = 0;
|
|
123
|
-
do {
|
|
124
|
-
pageCount++;
|
|
125
|
-
if (pageCount > ApiiroAnnotationProcessor.MAX_PAGES) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`Pagination limit exceeded: Maximum of ${ApiiroAnnotationProcessor.MAX_PAGES} pages allowed. This may indicate an infinite loop or an unexpectedly large dataset. Fetched ${items.length} repositories so far.`
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
const accessToken = this.getAccessToken();
|
|
131
|
-
if (!accessToken) {
|
|
132
|
-
console.warn(
|
|
133
|
-
"[ApiiroAnnotationProcessor] Apiiro access token not configured. Please set apiiro.accessToken in your app-config."
|
|
134
|
-
);
|
|
135
|
-
return {
|
|
136
|
-
items: [],
|
|
137
|
-
totalCount: 0
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
const page = await this.fetchRepositoriesPage(
|
|
141
|
-
accessToken,
|
|
142
|
-
nextCursor ?? void 0
|
|
143
|
-
);
|
|
144
|
-
items.push(...page.items);
|
|
145
|
-
nextCursor = page.next;
|
|
146
|
-
} while (nextCursor);
|
|
147
|
-
return { items, totalCount: items.length };
|
|
42
|
+
isApplicationsViewEnabled() {
|
|
43
|
+
return this.config.getOptionalBoolean("apiiro.enableApplicationsView") ?? false;
|
|
148
44
|
}
|
|
149
|
-
/**
|
|
150
|
-
* Optimized method that combines default branch selection and map building in a single loop.
|
|
151
|
-
* Groups repositories by URL, selects the best branch for each, and builds the URL->key mapping.
|
|
152
|
-
* @param repositories Array of all repository items
|
|
153
|
-
* @returns Map of repository URLs to repository keys
|
|
154
|
-
*/
|
|
155
|
-
buildRepositoryMapWithDefaultBranches(repositories) {
|
|
156
|
-
const repoMap = /* @__PURE__ */ new Map();
|
|
157
|
-
for (const repo of repositories) {
|
|
158
|
-
if (!repo.url) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (repoMap.has(repo.url)) {
|
|
162
|
-
if (repo.isDefaultBranch) {
|
|
163
|
-
repoMap.set(repo.url, {
|
|
164
|
-
key: repo.key,
|
|
165
|
-
isDefaultBranch: repo.isDefaultBranch
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
continue;
|
|
169
|
-
} else {
|
|
170
|
-
repoMap.set(repo.url, {
|
|
171
|
-
key: repo.key,
|
|
172
|
-
isDefaultBranch: repo.isDefaultBranch
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return repoMap;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Fetches all repositories and builds a name->key mapping.
|
|
180
|
-
* Automatically filters to default branches only.
|
|
181
|
-
* @returns Map of repository names to repository keys
|
|
182
|
-
*/
|
|
183
|
-
async fetchAllRepos() {
|
|
184
|
-
try {
|
|
185
|
-
const { items } = await this.fetchAllRepositories();
|
|
186
|
-
return this.buildRepositoryMapWithDefaultBranches(items);
|
|
187
|
-
} catch (error) {
|
|
188
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
189
|
-
console.error(
|
|
190
|
-
`[ApiiroAnnotationProcessor] Error fetching repositories from Apiiro API: ${errorMessage}`
|
|
191
|
-
);
|
|
192
|
-
return /* @__PURE__ */ new Map();
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Checks if the cache is expired.
|
|
197
|
-
*/
|
|
198
|
-
isCacheExpired() {
|
|
199
|
-
const now = Date.now();
|
|
200
|
-
return now - this.repoCache.lastFetched > ApiiroAnnotationProcessor.CACHE_TTL_MS || this.repoCache.data.size === 0;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Refreshes the repository cache from the Apiiro API.
|
|
204
|
-
* Prevents concurrent refreshes using a flag.
|
|
205
|
-
*/
|
|
206
|
-
async refreshCacheIfNeeded() {
|
|
207
|
-
if (!this.isCacheExpired() || this.repoCache.isRefreshing) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
this.repoCache.isRefreshing = true;
|
|
211
|
-
try {
|
|
212
|
-
const newData = await this.fetchAllRepos();
|
|
213
|
-
this.repoCache.data = newData;
|
|
214
|
-
this.repoCache.lastFetched = Date.now();
|
|
215
|
-
} finally {
|
|
216
|
-
this.repoCache.isRefreshing = false;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Retrieves the repository key for a given entity name.
|
|
221
|
-
* Uses a cached map that refreshes automatically when expired.
|
|
222
|
-
* @param entityName - The name of the entity/repository
|
|
223
|
-
* @returns Repository key or null if not found
|
|
224
|
-
*/
|
|
225
|
-
async getRepoKey(repoUrl) {
|
|
226
|
-
await this.refreshCacheIfNeeded();
|
|
227
|
-
return this.repoCache.data.get(repoUrl)?.key || null;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Creates an entity reference string in the format: kind:namespace/name
|
|
231
|
-
*/
|
|
232
|
-
createEntityReference(entity) {
|
|
233
|
-
const kind = entity.kind?.toLowerCase() || "component";
|
|
234
|
-
const namespace = entity.metadata.namespace || ApiiroAnnotationProcessor.DEFAULT_NAMESPACE;
|
|
235
|
-
const name = entity.metadata.name;
|
|
236
|
-
return `${kind}:${namespace}/${name}`;
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Determines if an entity should have the metrics view annotation.
|
|
240
|
-
* Based on permission control configuration (exclude/include list).
|
|
241
|
-
*/
|
|
242
45
|
shouldAllowMetricsView(entity) {
|
|
243
46
|
const exclude = this.config.getOptionalBoolean("apiiro.annotationControl.exclude") ?? true;
|
|
244
|
-
const entityNames = this.config.getOptionalStringArray(
|
|
245
|
-
"apiiro.annotationControl.entityNames"
|
|
246
|
-
) ?? [];
|
|
47
|
+
const entityNames = this.config.getOptionalStringArray("apiiro.annotationControl.entityNames")?.map((name) => name.toLowerCase()) ?? [];
|
|
247
48
|
if (entityNames.length === 0) {
|
|
248
49
|
return this.config.getOptionalBoolean("apiiro.defaultAllowMetricsView") ?? true;
|
|
249
50
|
}
|
|
250
|
-
const entityRef =
|
|
51
|
+
const entityRef = catalogModel.stringifyEntityRef(entity);
|
|
251
52
|
const isInList = entityNames.includes(entityRef);
|
|
252
53
|
return exclude ? !isInList : isInList;
|
|
253
54
|
}
|
|
254
|
-
|
|
255
|
-
* Adds Apiiro annotations to an entity if they don't already exist.
|
|
256
|
-
*/
|
|
257
|
-
addApiiroAnnotations(entity, repoKey, allowMetricsView) {
|
|
55
|
+
addApiiroAnnotations(entity, repoKey, allowMetricsView, applicationId) {
|
|
258
56
|
const annotations = {
|
|
259
57
|
...entity.metadata?.annotations
|
|
260
58
|
};
|
|
261
59
|
if (repoKey && !Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_PROJECT_ANNOTATION)) {
|
|
262
60
|
annotations[pluginApiiroCommon.APIIRO_PROJECT_ANNOTATION] = repoKey;
|
|
263
61
|
}
|
|
264
|
-
if (
|
|
62
|
+
if (applicationId && !Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_APPLICATION_ANNOTATION)) {
|
|
63
|
+
annotations[pluginApiiroCommon.APIIRO_APPLICATION_ANNOTATION] = applicationId;
|
|
64
|
+
}
|
|
65
|
+
if ((repoKey || Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_PROJECT_ANNOTATION) || applicationId || Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_APPLICATION_ANNOTATION)) && !Object.keys(annotations).includes(pluginApiiroCommon.APIIRO_METRICS_VIEW_ANNOTATION)) {
|
|
265
66
|
annotations[pluginApiiroCommon.APIIRO_METRICS_VIEW_ANNOTATION] = allowMetricsView ? "true" : "false";
|
|
266
67
|
}
|
|
267
68
|
return annotations;
|
|
268
69
|
}
|
|
269
|
-
/**
|
|
270
|
-
* Preprocesses an entity to add Apiiro-specific annotations.
|
|
271
|
-
* Only processes Component entities.
|
|
272
|
-
*/
|
|
273
70
|
async preProcessEntity(entity, _location, _emit, _originLocation, _cache) {
|
|
274
71
|
if (!this.shouldProcessEntity(entity)) {
|
|
275
72
|
return entity;
|
|
276
73
|
}
|
|
74
|
+
let repoKey = null;
|
|
75
|
+
let applicationId = null;
|
|
76
|
+
if (entity.kind === "Component") {
|
|
77
|
+
const sourceLocation = entity.metadata.annotations?.[types.BACKSTAGE_SOURCE_LOCATION_ANNOTATION];
|
|
78
|
+
const repoUrl = utils.extractRepoUrlFromSourceLocation(sourceLocation);
|
|
79
|
+
repoKey = repoUrl ? await this.cacheManager.getRepoKey(repoUrl) : null;
|
|
80
|
+
}
|
|
81
|
+
if (entity.kind === "System" && this.isApplicationsViewEnabled()) {
|
|
82
|
+
const entityRef = catalogModel.stringifyEntityRef(entity);
|
|
83
|
+
let entityUid = await this.cacheManager.getEntityUid(entityRef);
|
|
84
|
+
if (!entityUid) {
|
|
85
|
+
await this.cacheManager.invalidateEntityRefCache();
|
|
86
|
+
entityUid = await this.cacheManager.getEntityUid(entityRef);
|
|
87
|
+
}
|
|
88
|
+
if (entityUid) {
|
|
89
|
+
applicationId = await this.cacheManager.getApplicationId(entityUid);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
277
92
|
const allowMetricsView = this.shouldAllowMetricsView(entity);
|
|
278
|
-
const sourceLocation = entity.metadata.annotations?.[BACKSTAGE_SOURCE_LOCATION_ANNOTATION];
|
|
279
|
-
const repoUrl = this.extractRepoUrlFromSourceLocation(sourceLocation);
|
|
280
|
-
const repoKey = repoUrl ? await this.getRepoKey(repoUrl) : null;
|
|
281
93
|
const annotations = this.addApiiroAnnotations(
|
|
282
94
|
entity,
|
|
283
95
|
repoKey,
|
|
284
|
-
allowMetricsView
|
|
96
|
+
allowMetricsView,
|
|
97
|
+
applicationId
|
|
285
98
|
);
|
|
286
99
|
return {
|
|
287
100
|
...entity,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiiroAnnotationProcessor.cjs.js","sources":["../../src/processor/ApiiroAnnotationProcessor.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n CatalogProcessor,\n CatalogProcessorEmit,\n CatalogProcessorCache,\n} from '@backstage/plugin-catalog-node';\nimport type { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { Entity } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport fetch from 'node-fetch';\nimport {\n APIIRO_METRICS_VIEW_ANNOTATION,\n APIIRO_PROJECT_ANNOTATION,\n APIIRO_DEFAULT_BASE_URL,\n} from '@backstage-community/plugin-apiiro-common';\n\ninterface RepositoryItem {\n name?: string | null;\n key?: string | null;\n url?: string | null;\n isDefaultBranch?: boolean;\n branchName?: string | null;\n [key: string]: unknown;\n}\n\ninterface ApiiroRepositoriesResponse {\n items: RepositoryItem[];\n next?: string | null;\n}\n\ninterface RepoCache {\n data: Map<string, { key: string; isDefaultBranch: boolean }>;\n lastFetched: number;\n isRefreshing: boolean;\n}\n\nconst BACKSTAGE_SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location';\n\nexport class ApiiroAnnotationProcessor implements CatalogProcessor {\n private static readonly CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour\n private static readonly PAGE_LIMIT = 1000;\n private static readonly MAX_PAGES = 1000;\n private static readonly DEFAULT_NAMESPACE = 'default';\n\n private repoCache: RepoCache = {\n data: new Map(),\n lastFetched: 0,\n isRefreshing: false,\n };\n\n constructor(private readonly config: Config) {}\n\n getProcessorName(): string {\n return 'ApiiroAnnotationProcessor';\n }\n\n /**\n * Determines if an entity should be processed by this processor.\n * Only Component entities are processed.\n */\n private shouldProcessEntity(entity: Entity): boolean {\n return entity.kind === 'Component';\n }\n\n private extractRepoUrlFromSourceLocation(\n sourceLocation?: string,\n ): string | null {\n try {\n if (!sourceLocation) {\n return null;\n }\n\n const matches = sourceLocation.match(\n /https?:\\/\\/[a-zA-Z0-9\\-.]+\\.[a-zA-Z]{2,}(:[0-9]{1,5})?(\\/.*)?/g,\n );\n\n if (!matches) {\n return null;\n }\n\n const url = new URL(matches[0]);\n const hostname = url.host.toLowerCase();\n\n let repoPath: string | null = null;\n\n if (hostname === 'dev.azure.com') {\n const pathMatch = url.pathname.match(\n /^\\/([^/]+)\\/([^/]+)\\/_git\\/([^/]+)(?:\\/.*)?$/,\n );\n if (pathMatch) {\n repoPath = `/${pathMatch[1]}/${pathMatch[2]}/_git/${pathMatch[3]}`;\n }\n } else if (hostname.includes('gitlab')) {\n // GitLab can have nested subgroups: /group/subgroup1/subgroup2/.../project\n // Remove /-/... suffix first (GitLab file/blob paths), then extract repo path\n const cleanPath = url.pathname.replace(/\\/-\\/.*$/, '');\n const pathMatch = cleanPath.match(/^(\\/[^/]+\\/[^/]+(?:\\/[^/]+)*)$/);\n if (pathMatch) {\n repoPath = pathMatch[1];\n }\n } else {\n // GitHub and other providers: /org/repo\n const pathMatch = url.pathname.match(/^\\/([^/]+)\\/([^/]+)(?:\\/.*)?$/);\n if (pathMatch) {\n repoPath = `/${pathMatch[1]}/${pathMatch[2]}`;\n }\n }\n\n if (!repoPath) {\n return null;\n }\n\n return `${url.protocol}//${hostname}${repoPath}`;\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Retrieves the Apiiro access token from configuration.\n * @throws {Error} If the access token is not configured\n */\n private getAccessToken(): string | undefined {\n const accessToken = this.config.getOptionalString('apiiro.accessToken');\n return accessToken;\n }\n\n /**\n * Fetches a single page of repositories from the Apiiro API.\n * @param pageCursor - Optional cursor for pagination\n * @returns Page of repositories with next cursor\n */\n private async fetchRepositoriesPage(\n accessToken: string,\n pageCursor?: string,\n ): Promise<ApiiroRepositoriesResponse> {\n const baseUrl = APIIRO_DEFAULT_BASE_URL;\n\n const params = new URLSearchParams();\n params.append('limit', ApiiroAnnotationProcessor.PAGE_LIMIT.toString());\n if (pageCursor) {\n params.append('next', pageCursor);\n }\n\n const url = `${baseUrl}/rest-api/v2/repositories?${params.toString()}`;\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const errorMessage = `Failed to fetch repositories from Apiiro API. Status: ${response.status} ${response.statusText}`;\n throw new Error(errorMessage);\n }\n\n const data = (await response.json()) as ApiiroRepositoriesResponse;\n return {\n items: data.items || [],\n next: data.next || null,\n };\n }\n\n /**\n * Fetches all repositories from the Apiiro API with pagination.\n * @returns All repositories with total count\n * @throws {Error} If pagination limit is exceeded\n */\n private async fetchAllRepositories(): Promise<{\n items: RepositoryItem[];\n totalCount: number;\n }> {\n const items: RepositoryItem[] = [];\n let nextCursor: string | null | undefined = undefined;\n let pageCount = 0;\n\n do {\n pageCount++;\n\n if (pageCount > ApiiroAnnotationProcessor.MAX_PAGES) {\n throw new Error(\n `Pagination limit exceeded: Maximum of ${ApiiroAnnotationProcessor.MAX_PAGES} pages allowed. ` +\n `This may indicate an infinite loop or an unexpectedly large dataset. ` +\n `Fetched ${items.length} repositories so far.`,\n );\n }\n\n const accessToken = this.getAccessToken();\n if (!accessToken) {\n console.warn(\n '[ApiiroAnnotationProcessor] Apiiro access token not configured. Please set apiiro.accessToken in your app-config.',\n );\n return {\n items: [],\n totalCount: 0,\n };\n }\n const page = await this.fetchRepositoriesPage(\n accessToken,\n nextCursor ?? undefined,\n );\n items.push(...page.items);\n nextCursor = page.next;\n } while (nextCursor);\n\n return { items, totalCount: items.length };\n }\n\n /**\n * Optimized method that combines default branch selection and map building in a single loop.\n * Groups repositories by URL, selects the best branch for each, and builds the URL->key mapping.\n * @param repositories Array of all repository items\n * @returns Map of repository URLs to repository keys\n */\n private buildRepositoryMapWithDefaultBranches(\n repositories: RepositoryItem[],\n ): Map<string, { key: string; isDefaultBranch: boolean }> {\n const repoMap = new Map<\n string,\n { key: string; isDefaultBranch: boolean }\n >();\n\n // Single loop: Group repositories by URL\n for (const repo of repositories) {\n if (!repo.url) {\n continue; // Skip items without URL\n }\n if (repoMap.has(repo.url)) {\n if (repo.isDefaultBranch) {\n repoMap.set(repo.url, {\n key: repo.key!,\n isDefaultBranch: repo.isDefaultBranch!,\n });\n }\n continue;\n } else {\n repoMap.set(repo.url, {\n key: repo.key!,\n isDefaultBranch: repo.isDefaultBranch!,\n });\n }\n }\n\n return repoMap;\n }\n\n /**\n * Fetches all repositories and builds a name->key mapping.\n * Automatically filters to default branches only.\n * @returns Map of repository names to repository keys\n */\n private async fetchAllRepos(): Promise<\n Map<string, { key: string; isDefaultBranch: boolean }>\n > {\n try {\n // Fetch all repositories with pagination\n const { items } = await this.fetchAllRepositories();\n\n // Combine default branch selection and map building in a single operation\n return this.buildRepositoryMapWithDefaultBranches(items);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n console.error(\n `[ApiiroAnnotationProcessor] Error fetching repositories from Apiiro API: ${errorMessage}`,\n );\n return new Map();\n }\n }\n\n /**\n * Checks if the cache is expired.\n */\n private isCacheExpired(): boolean {\n const now = Date.now();\n return (\n now - this.repoCache.lastFetched >\n ApiiroAnnotationProcessor.CACHE_TTL_MS || this.repoCache.data.size === 0\n );\n }\n\n /**\n * Refreshes the repository cache from the Apiiro API.\n * Prevents concurrent refreshes using a flag.\n */\n private async refreshCacheIfNeeded(): Promise<void> {\n if (!this.isCacheExpired() || this.repoCache.isRefreshing) {\n return;\n }\n\n this.repoCache.isRefreshing = true;\n\n try {\n const newData = await this.fetchAllRepos();\n this.repoCache.data = newData;\n this.repoCache.lastFetched = Date.now();\n } finally {\n this.repoCache.isRefreshing = false;\n }\n }\n\n /**\n * Retrieves the repository key for a given entity name.\n * Uses a cached map that refreshes automatically when expired.\n * @param entityName - The name of the entity/repository\n * @returns Repository key or null if not found\n */\n private async getRepoKey(repoUrl: string): Promise<string | null> {\n await this.refreshCacheIfNeeded();\n return this.repoCache.data.get(repoUrl)?.key || null;\n }\n\n /**\n * Creates an entity reference string in the format: kind:namespace/name\n */\n private createEntityReference(entity: Entity): string {\n const kind = entity.kind?.toLowerCase() || 'component';\n const namespace =\n entity.metadata.namespace || ApiiroAnnotationProcessor.DEFAULT_NAMESPACE;\n const name = entity.metadata.name;\n return `${kind}:${namespace}/${name}`;\n }\n\n /**\n * Determines if an entity should have the metrics view annotation.\n * Based on permission control configuration (exclude/include list).\n */\n private shouldAllowMetricsView(entity: Entity): boolean {\n const exclude =\n this.config.getOptionalBoolean('apiiro.annotationControl.exclude') ??\n true;\n const entityNames =\n this.config.getOptionalStringArray(\n 'apiiro.annotationControl.entityNames',\n ) ?? [];\n\n if (entityNames.length === 0) {\n return (\n this.config.getOptionalBoolean('apiiro.defaultAllowMetricsView') ?? true\n );\n }\n\n const entityRef = this.createEntityReference(entity);\n const isInList = entityNames.includes(entityRef);\n\n // If exclude=true: allow all except those in list\n // If exclude=false: allow only those in list\n return exclude ? !isInList : isInList;\n }\n\n /**\n * Adds Apiiro annotations to an entity if they don't already exist.\n */\n private addApiiroAnnotations(\n entity: Entity,\n repoKey: string | null,\n allowMetricsView: boolean,\n ): Record<string, string> {\n const annotations: Record<string, string> = {\n ...entity.metadata?.annotations,\n };\n\n // Add project annotation if repo key exists and annotation not already set\n if (\n repoKey &&\n !Object.keys(annotations).includes(APIIRO_PROJECT_ANNOTATION)\n ) {\n annotations[APIIRO_PROJECT_ANNOTATION] = repoKey;\n }\n\n // Add metrics view annotation if allowed and not already set\n if (\n (repoKey ||\n Object.keys(annotations).includes(APIIRO_PROJECT_ANNOTATION)) &&\n !Object.keys(annotations).includes(APIIRO_METRICS_VIEW_ANNOTATION)\n ) {\n annotations[APIIRO_METRICS_VIEW_ANNOTATION] = allowMetricsView\n ? 'true'\n : 'false';\n }\n\n return annotations;\n }\n\n /**\n * Preprocesses an entity to add Apiiro-specific annotations.\n * Only processes Component entities.\n */\n async preProcessEntity(\n entity: Entity,\n _location: LocationSpec,\n _emit: CatalogProcessorEmit,\n _originLocation: LocationSpec,\n _cache: CatalogProcessorCache,\n ): Promise<Entity> {\n if (!this.shouldProcessEntity(entity)) {\n return entity;\n }\n\n // Determine if metrics view should be allowed\n const allowMetricsView = this.shouldAllowMetricsView(entity);\n\n const sourceLocation =\n entity.metadata.annotations?.[BACKSTAGE_SOURCE_LOCATION_ANNOTATION];\n const repoUrl = this.extractRepoUrlFromSourceLocation(sourceLocation);\n\n // Get repository key from cache (refreshes automatically if needed)\n const repoKey = repoUrl ? await this.getRepoKey(repoUrl) : null;\n\n // Add Apiiro annotations\n const annotations = this.addApiiroAnnotations(\n entity,\n repoKey,\n allowMetricsView,\n );\n\n return {\n ...entity,\n metadata: {\n ...entity.metadata,\n annotations,\n },\n };\n }\n}\n"],"names":["APIIRO_DEFAULT_BASE_URL","fetch","APIIRO_PROJECT_ANNOTATION","APIIRO_METRICS_VIEW_ANNOTATION"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ApiiroAnnotationProcessor.cjs.js","sources":["../../src/processor/ApiiroAnnotationProcessor.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n CatalogProcessor,\n CatalogProcessorEmit,\n CatalogProcessorCache,\n} from '@backstage/plugin-catalog-node';\nimport type { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { CatalogApi } from '@backstage/catalog-client';\nimport { Entity, stringifyEntityRef } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { AuthService, CacheService } from '@backstage/backend-plugin-api';\nimport {\n APIIRO_METRICS_VIEW_ANNOTATION,\n APIIRO_PROJECT_ANNOTATION,\n APIIRO_APPLICATION_ANNOTATION,\n} from '@backstage-community/plugin-apiiro-common';\nimport { ApiiroApiClient } from '../helpers/apiClient';\nimport { CacheManager } from '../helpers/cacheManager';\nimport { extractRepoUrlFromSourceLocation } from '../helpers/utils';\nimport { BACKSTAGE_SOURCE_LOCATION_ANNOTATION } from '../helpers/types';\n\nexport class ApiiroAnnotationProcessor implements CatalogProcessor {\n private readonly apiClient: ApiiroApiClient;\n private readonly cacheManager: CacheManager;\n private readonly catalogApi?: CatalogApi;\n private readonly auth?: AuthService;\n private readonly cache: CacheService;\n\n constructor(\n private readonly config: Config,\n options: {\n catalogApi?: CatalogApi;\n auth?: AuthService;\n cache: CacheService;\n },\n ) {\n const accessToken = this.config.getOptionalString('apiiro.accessToken');\n this.apiClient = new ApiiroApiClient(accessToken);\n this.catalogApi = options?.catalogApi;\n this.auth = options?.auth;\n this.cache = options.cache;\n this.cacheManager = new CacheManager(\n this.apiClient,\n this.cache,\n this.catalogApi,\n this.auth,\n );\n }\n\n getProcessorName(): string {\n return 'ApiiroAnnotationProcessor';\n }\n\n private shouldProcessEntity(entity: Entity): boolean {\n if (entity.kind === 'Component') {\n return true;\n }\n if (entity.kind === 'System' && this.isApplicationsViewEnabled()) {\n return true;\n }\n return false;\n }\n\n private isApplicationsViewEnabled(): boolean {\n return (\n this.config.getOptionalBoolean('apiiro.enableApplicationsView') ?? false\n );\n }\n\n private shouldAllowMetricsView(entity: Entity): boolean {\n const exclude =\n this.config.getOptionalBoolean('apiiro.annotationControl.exclude') ??\n true;\n const entityNames =\n this.config\n .getOptionalStringArray('apiiro.annotationControl.entityNames')\n ?.map(name => name.toLowerCase()) ?? [];\n\n if (entityNames.length === 0) {\n return (\n this.config.getOptionalBoolean('apiiro.defaultAllowMetricsView') ?? true\n );\n }\n\n const entityRef = stringifyEntityRef(entity);\n const isInList = entityNames.includes(entityRef);\n\n return exclude ? !isInList : isInList;\n }\n\n private addApiiroAnnotations(\n entity: Entity,\n repoKey: string | null,\n allowMetricsView: boolean,\n applicationId: string | null,\n ): Record<string, string> {\n const annotations: Record<string, string> = {\n ...entity.metadata?.annotations,\n };\n\n if (\n repoKey &&\n !Object.keys(annotations).includes(APIIRO_PROJECT_ANNOTATION)\n ) {\n annotations[APIIRO_PROJECT_ANNOTATION] = repoKey;\n }\n\n if (\n applicationId &&\n !Object.keys(annotations).includes(APIIRO_APPLICATION_ANNOTATION)\n ) {\n annotations[APIIRO_APPLICATION_ANNOTATION] = applicationId;\n }\n\n if (\n (repoKey ||\n Object.keys(annotations).includes(APIIRO_PROJECT_ANNOTATION) ||\n applicationId ||\n Object.keys(annotations).includes(APIIRO_APPLICATION_ANNOTATION)) &&\n !Object.keys(annotations).includes(APIIRO_METRICS_VIEW_ANNOTATION)\n ) {\n annotations[APIIRO_METRICS_VIEW_ANNOTATION] = allowMetricsView\n ? 'true'\n : 'false';\n }\n\n return annotations;\n }\n\n async preProcessEntity(\n entity: Entity,\n _location: LocationSpec,\n _emit: CatalogProcessorEmit,\n _originLocation: LocationSpec,\n _cache: CatalogProcessorCache,\n ): Promise<Entity> {\n if (!this.shouldProcessEntity(entity)) {\n return entity;\n }\n\n let repoKey: string | null = null;\n let applicationId: string | null = null;\n\n if (entity.kind === 'Component') {\n const sourceLocation =\n entity.metadata.annotations?.[BACKSTAGE_SOURCE_LOCATION_ANNOTATION];\n const repoUrl = extractRepoUrlFromSourceLocation(sourceLocation);\n repoKey = repoUrl ? await this.cacheManager.getRepoKey(repoUrl) : null;\n }\n\n if (entity.kind === 'System' && this.isApplicationsViewEnabled()) {\n const entityRef = stringifyEntityRef(entity);\n let entityUid = await this.cacheManager.getEntityUid(entityRef);\n\n if (!entityUid) {\n await this.cacheManager.invalidateEntityRefCache();\n entityUid = await this.cacheManager.getEntityUid(entityRef);\n }\n\n if (entityUid) {\n applicationId = await this.cacheManager.getApplicationId(entityUid);\n }\n }\n\n const allowMetricsView = this.shouldAllowMetricsView(entity);\n\n const annotations = this.addApiiroAnnotations(\n entity,\n repoKey,\n allowMetricsView,\n applicationId,\n );\n\n return {\n ...entity,\n metadata: {\n ...entity.metadata,\n annotations,\n },\n };\n }\n}\n"],"names":["ApiiroApiClient","CacheManager","stringifyEntityRef","APIIRO_PROJECT_ANNOTATION","APIIRO_APPLICATION_ANNOTATION","APIIRO_METRICS_VIEW_ANNOTATION","BACKSTAGE_SOURCE_LOCATION_ANNOTATION","extractRepoUrlFromSourceLocation"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage-community/plugin-catalog-backend-module-apiiro-entity-processor",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "The apiiro-entity-processor backend module for the catalog plugin.",
|
|
6
6
|
"main": "dist/index.cjs.js",
|
|
@@ -33,16 +33,18 @@
|
|
|
33
33
|
"postpack": "backstage-cli package postpack"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@backstage-community/plugin-apiiro-common": "^
|
|
37
|
-
"@backstage/backend-plugin-api": "^1.
|
|
38
|
-
"@backstage/catalog-
|
|
39
|
-
"@backstage/
|
|
40
|
-
"@backstage/
|
|
41
|
-
"@backstage/plugin-catalog-
|
|
42
|
-
"node
|
|
36
|
+
"@backstage-community/plugin-apiiro-common": "^1.0.0",
|
|
37
|
+
"@backstage/backend-plugin-api": "^1.7.0",
|
|
38
|
+
"@backstage/catalog-client": "^1.13.0",
|
|
39
|
+
"@backstage/catalog-model": "^1.7.6",
|
|
40
|
+
"@backstage/config": "^1.3.6",
|
|
41
|
+
"@backstage/plugin-catalog-common": "^1.1.8",
|
|
42
|
+
"@backstage/plugin-catalog-node": "^2.0.0",
|
|
43
|
+
"node-fetch": "^2.6.7",
|
|
44
|
+
"path-to-regexp": "^8.0.0"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
|
-
"@backstage/cli": "^0.
|
|
47
|
+
"@backstage/cli": "^0.35.4",
|
|
46
48
|
"@types/node-fetch": "^2.6.9"
|
|
47
49
|
},
|
|
48
50
|
"files": [
|