@backstage/plugin-techdocs-backend 0.10.9 → 0.12.2
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 +70 -0
- package/config.d.ts +26 -0
- package/dist/index.cjs.js +228 -63
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/package.json +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# @backstage/plugin-techdocs-backend
|
|
2
2
|
|
|
3
|
+
## 0.12.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- da676a49ab: Add support for API auth in DefaultTechDocsCollator
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @backstage/techdocs-common@0.11.2
|
|
10
|
+
- @backstage/backend-common@0.10.1
|
|
11
|
+
- @backstage/integration@0.7.0
|
|
12
|
+
|
|
13
|
+
## 0.12.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 8c25c3ea5b: Fixed a bug preventing cache from being enabled in TechDocs "recommended" deployment mode.
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @backstage/backend-common@0.10.0
|
|
20
|
+
- @backstage/catalog-client@0.5.3
|
|
21
|
+
- @backstage/techdocs-common@0.11.1
|
|
22
|
+
|
|
23
|
+
## 0.12.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- 1bada775a9: Added the ability for the TechDocs Backend to (optionally) leverage a cache
|
|
28
|
+
store to improve performance when reading files from a cloud storage provider.
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly
|
|
33
|
+
- Updated dependencies
|
|
34
|
+
- @backstage/backend-common@0.9.13
|
|
35
|
+
- @backstage/techdocs-common@0.11.0
|
|
36
|
+
|
|
37
|
+
## 0.11.0
|
|
38
|
+
|
|
39
|
+
### Minor Changes
|
|
40
|
+
|
|
41
|
+
- 905dd952ac: **BREAKING** `DefaultTechDocsCollator` has a new required option `tokenManager`. See the create-app changelog for how to create a `tokenManager` and add it to the `PluginEnvironment`. It can then be passed to the collator in `createPlugin`:
|
|
42
|
+
|
|
43
|
+
```diff
|
|
44
|
+
// packages/backend/src/plugins/search.ts
|
|
45
|
+
|
|
46
|
+
...
|
|
47
|
+
export default async function createPlugin({
|
|
48
|
+
...
|
|
49
|
+
+ tokenManager,
|
|
50
|
+
}: PluginEnvironment) {
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
indexBuilder.addCollator({
|
|
54
|
+
defaultRefreshIntervalSeconds: 600,
|
|
55
|
+
collator: DefaultTechDocsCollator.fromConfig(config, {
|
|
56
|
+
discovery,
|
|
57
|
+
logger,
|
|
58
|
+
+ tokenManager,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
...
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them
|
|
69
|
+
- Updated dependencies
|
|
70
|
+
- @backstage/integration@0.6.10
|
|
71
|
+
- @backstage/backend-common@0.9.12
|
|
72
|
+
|
|
3
73
|
## 0.10.9
|
|
4
74
|
|
|
5
75
|
### Patch Changes
|
package/config.d.ts
CHANGED
|
@@ -226,6 +226,32 @@ export interface Config {
|
|
|
226
226
|
};
|
|
227
227
|
};
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* @example http://localhost:7007/api/techdocs
|
|
231
|
+
* Techdocs cache information
|
|
232
|
+
*/
|
|
233
|
+
cache?: {
|
|
234
|
+
/**
|
|
235
|
+
* The cache time-to-live for TechDocs sites (in milliseconds). Set this
|
|
236
|
+
* to a non-zero value to cache TechDocs sites and assets as they are
|
|
237
|
+
* read from storage.
|
|
238
|
+
*
|
|
239
|
+
* Note: you must also configure `backend.cache` appropriately as well,
|
|
240
|
+
* and to pass a PluginCacheManager instance to TechDocs Backend's
|
|
241
|
+
* createRouter method in your backend.
|
|
242
|
+
*/
|
|
243
|
+
ttl: number;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The time (in milliseconds) that the TechDocs backend will wait for
|
|
247
|
+
* a cache service to respond before continuing on as though the cached
|
|
248
|
+
* object was not found (e.g. when the cache sercice is unavailable).
|
|
249
|
+
*
|
|
250
|
+
* Defaults to 1000 milliseconds.
|
|
251
|
+
*/
|
|
252
|
+
readTimeout?: number;
|
|
253
|
+
};
|
|
254
|
+
|
|
229
255
|
/**
|
|
230
256
|
* @example http://localhost:7007/api/techdocs
|
|
231
257
|
* @visibility frontend
|
package/dist/index.cjs.js
CHANGED
|
@@ -6,9 +6,10 @@ var catalogClient = require('@backstage/catalog-client');
|
|
|
6
6
|
var catalogModel = require('@backstage/catalog-model');
|
|
7
7
|
var errors = require('@backstage/errors');
|
|
8
8
|
var techdocsCommon = require('@backstage/techdocs-common');
|
|
9
|
-
var fetch = require('
|
|
10
|
-
var
|
|
9
|
+
var fetch$1 = require('node-fetch');
|
|
10
|
+
var router = require('express-promise-router');
|
|
11
11
|
var integration = require('@backstage/integration');
|
|
12
|
+
var fetch = require('cross-fetch');
|
|
12
13
|
var stream = require('stream');
|
|
13
14
|
var winston = require('winston');
|
|
14
15
|
var fs = require('fs-extra');
|
|
@@ -28,19 +29,18 @@ function _interopNamespace(e) {
|
|
|
28
29
|
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
29
30
|
Object.defineProperty(n, k, d.get ? d : {
|
|
30
31
|
enumerable: true,
|
|
31
|
-
get: function () {
|
|
32
|
-
return e[k];
|
|
33
|
-
}
|
|
32
|
+
get: function () { return e[k]; }
|
|
34
33
|
});
|
|
35
34
|
}
|
|
36
35
|
});
|
|
37
36
|
}
|
|
38
|
-
n[
|
|
37
|
+
n["default"] = e;
|
|
39
38
|
return Object.freeze(n);
|
|
40
39
|
}
|
|
41
40
|
|
|
41
|
+
var fetch__default$1 = /*#__PURE__*/_interopDefaultLegacy(fetch$1);
|
|
42
|
+
var router__default = /*#__PURE__*/_interopDefaultLegacy(router);
|
|
42
43
|
var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
|
|
43
|
-
var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
|
|
44
44
|
var winston__namespace = /*#__PURE__*/_interopNamespace(winston);
|
|
45
45
|
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
46
46
|
var os__default = /*#__PURE__*/_interopDefaultLegacy(os);
|
|
@@ -80,7 +80,8 @@ class DocsBuilder {
|
|
|
80
80
|
logger,
|
|
81
81
|
config,
|
|
82
82
|
scmIntegrations,
|
|
83
|
-
logStream
|
|
83
|
+
logStream,
|
|
84
|
+
cache
|
|
84
85
|
}) {
|
|
85
86
|
this.preparer = preparers.get(entity);
|
|
86
87
|
this.generator = generators.get(entity);
|
|
@@ -90,9 +91,10 @@ class DocsBuilder {
|
|
|
90
91
|
this.config = config;
|
|
91
92
|
this.scmIntegrations = scmIntegrations;
|
|
92
93
|
this.logStream = logStream;
|
|
94
|
+
this.cache = cache;
|
|
93
95
|
}
|
|
94
96
|
async build() {
|
|
95
|
-
var _a;
|
|
97
|
+
var _a, _b;
|
|
96
98
|
if (!this.entity.metadata.uid) {
|
|
97
99
|
throw new Error("Trying to build documentation for entity not in software catalog");
|
|
98
100
|
}
|
|
@@ -129,9 +131,9 @@ class DocsBuilder {
|
|
|
129
131
|
this.logger.info(`Prepare step completed for entity ${catalogModel.stringifyEntityRef(this.entity)}, stored at ${preparedDir}`);
|
|
130
132
|
this.logger.info(`Step 2 of 3: Generating docs for entity ${catalogModel.stringifyEntityRef(this.entity)}`);
|
|
131
133
|
const workingDir = this.config.getOptionalString("backend.workingDirectory");
|
|
132
|
-
const tmpdirPath = workingDir || os__default[
|
|
133
|
-
const tmpdirResolvedPath = fs__default[
|
|
134
|
-
const outputDir = await fs__default[
|
|
134
|
+
const tmpdirPath = workingDir || os__default["default"].tmpdir();
|
|
135
|
+
const tmpdirResolvedPath = fs__default["default"].realpathSync(tmpdirPath);
|
|
136
|
+
const outputDir = await fs__default["default"].mkdtemp(path__default["default"].join(tmpdirResolvedPath, "techdocs-tmp-"));
|
|
135
137
|
const parsedLocationAnnotation = techdocsCommon.getLocationForEntity(this.entity, this.scmIntegrations);
|
|
136
138
|
await this.generator.run({
|
|
137
139
|
inputDir: preparedDir,
|
|
@@ -144,19 +146,23 @@ class DocsBuilder {
|
|
|
144
146
|
if (this.preparer instanceof techdocsCommon.UrlPreparer) {
|
|
145
147
|
this.logger.debug(`Removing prepared directory ${preparedDir} since the site has been generated`);
|
|
146
148
|
try {
|
|
147
|
-
fs__default[
|
|
149
|
+
fs__default["default"].remove(preparedDir);
|
|
148
150
|
} catch (error) {
|
|
149
151
|
errors.assertError(error);
|
|
150
152
|
this.logger.debug(`Error removing prepared directory ${error.message}`);
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
this.logger.info(`Step 3 of 3: Publishing docs for entity ${catalogModel.stringifyEntityRef(this.entity)}`);
|
|
154
|
-
await this.publisher.publish({
|
|
156
|
+
const published = await this.publisher.publish({
|
|
155
157
|
entity: this.entity,
|
|
156
158
|
directory: outputDir
|
|
157
159
|
});
|
|
160
|
+
if (this.cache && published && ((_b = published == null ? void 0 : published.objects) == null ? void 0 : _b.length)) {
|
|
161
|
+
this.logger.debug(`Invalidating ${published.objects.length} cache objects`);
|
|
162
|
+
await this.cache.invalidateMultiple(published.objects);
|
|
163
|
+
}
|
|
158
164
|
try {
|
|
159
|
-
fs__default[
|
|
165
|
+
fs__default["default"].remove(outputDir);
|
|
160
166
|
this.logger.debug(`Removing generated directory ${outputDir} since the site has been published`);
|
|
161
167
|
} catch (error) {
|
|
162
168
|
errors.assertError(error);
|
|
@@ -172,15 +178,17 @@ class DocsSynchronizer {
|
|
|
172
178
|
publisher,
|
|
173
179
|
logger,
|
|
174
180
|
config,
|
|
175
|
-
scmIntegrations
|
|
181
|
+
scmIntegrations,
|
|
182
|
+
cache
|
|
176
183
|
}) {
|
|
177
184
|
this.config = config;
|
|
178
185
|
this.logger = logger;
|
|
179
186
|
this.publisher = publisher;
|
|
180
187
|
this.scmIntegrations = scmIntegrations;
|
|
188
|
+
this.cache = cache;
|
|
181
189
|
}
|
|
182
190
|
async doSync({
|
|
183
|
-
responseHandler: {log, error, finish},
|
|
191
|
+
responseHandler: { log, error, finish },
|
|
184
192
|
entity,
|
|
185
193
|
preparers,
|
|
186
194
|
generators
|
|
@@ -194,9 +202,9 @@ class DocsSynchronizer {
|
|
|
194
202
|
logStream.on("data", async (data) => {
|
|
195
203
|
log(data.toString().trim());
|
|
196
204
|
});
|
|
197
|
-
taskLogger.add(new winston__namespace.transports.Stream({stream: logStream}));
|
|
205
|
+
taskLogger.add(new winston__namespace.transports.Stream({ stream: logStream }));
|
|
198
206
|
if (!shouldCheckForUpdate(entity.metadata.uid)) {
|
|
199
|
-
finish({updated: false});
|
|
207
|
+
finish({ updated: false });
|
|
200
208
|
return;
|
|
201
209
|
}
|
|
202
210
|
let foundDocs = false;
|
|
@@ -209,11 +217,12 @@ class DocsSynchronizer {
|
|
|
209
217
|
entity,
|
|
210
218
|
config: this.config,
|
|
211
219
|
scmIntegrations: this.scmIntegrations,
|
|
212
|
-
logStream
|
|
220
|
+
logStream,
|
|
221
|
+
cache: this.cache
|
|
213
222
|
});
|
|
214
223
|
const updated = await docsBuilder.build();
|
|
215
224
|
if (!updated) {
|
|
216
|
-
finish({updated: false});
|
|
225
|
+
finish({ updated: false });
|
|
217
226
|
return;
|
|
218
227
|
}
|
|
219
228
|
} catch (e) {
|
|
@@ -236,7 +245,146 @@ class DocsSynchronizer {
|
|
|
236
245
|
error(new errors.NotFoundError("Sorry! It took too long for the generated docs to show up in storage. Check back later."));
|
|
237
246
|
return;
|
|
238
247
|
}
|
|
239
|
-
finish({updated: true});
|
|
248
|
+
finish({ updated: true });
|
|
249
|
+
}
|
|
250
|
+
async doCacheSync({
|
|
251
|
+
responseHandler: { finish },
|
|
252
|
+
discovery,
|
|
253
|
+
token,
|
|
254
|
+
entity
|
|
255
|
+
}) {
|
|
256
|
+
var _a;
|
|
257
|
+
if (!shouldCheckForUpdate(entity.metadata.uid) || !this.cache) {
|
|
258
|
+
finish({ updated: false });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const baseUrl = await discovery.getBaseUrl("techdocs");
|
|
262
|
+
const namespace = ((_a = entity.metadata) == null ? void 0 : _a.namespace) || catalogModel.ENTITY_DEFAULT_NAMESPACE;
|
|
263
|
+
const kind = entity.kind;
|
|
264
|
+
const name = entity.metadata.name;
|
|
265
|
+
const legacyPathCasing = this.config.getOptionalBoolean("techdocs.legacyUseCaseSensitiveTripletPaths") || false;
|
|
266
|
+
const tripletPath = `${namespace}/${kind}/${name}`;
|
|
267
|
+
const entityTripletPath = `${legacyPathCasing ? tripletPath : tripletPath.toLocaleLowerCase("en-US")}`;
|
|
268
|
+
try {
|
|
269
|
+
const [sourceMetadata, cachedMetadata] = await Promise.all([
|
|
270
|
+
this.publisher.fetchTechDocsMetadata({ namespace, kind, name }),
|
|
271
|
+
fetch__default["default"](`${baseUrl}/static/docs/${entityTripletPath}/techdocs_metadata.json`, {
|
|
272
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
273
|
+
}).then((f) => f.json().catch(() => void 0))
|
|
274
|
+
]);
|
|
275
|
+
if (sourceMetadata.build_timestamp !== cachedMetadata.build_timestamp) {
|
|
276
|
+
const files = [
|
|
277
|
+
.../* @__PURE__ */ new Set([
|
|
278
|
+
...sourceMetadata.files || [],
|
|
279
|
+
...cachedMetadata.files || []
|
|
280
|
+
])
|
|
281
|
+
].map((f) => `${entityTripletPath}/${f}`);
|
|
282
|
+
await this.cache.invalidateMultiple(files);
|
|
283
|
+
finish({ updated: true });
|
|
284
|
+
} else {
|
|
285
|
+
finish({ updated: false });
|
|
286
|
+
}
|
|
287
|
+
} catch (e) {
|
|
288
|
+
errors.assertError(e);
|
|
289
|
+
this.logger.error(`Error syncing cache for ${entityTripletPath}: ${e.message}`);
|
|
290
|
+
finish({ updated: false });
|
|
291
|
+
} finally {
|
|
292
|
+
new BuildMetadataStorage(entity.metadata.uid).setLastUpdated();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const createCacheMiddleware = ({
|
|
298
|
+
cache
|
|
299
|
+
}) => {
|
|
300
|
+
const cacheMiddleware = router__default["default"]();
|
|
301
|
+
cacheMiddleware.use(async (req, res, next) => {
|
|
302
|
+
const socket = res.socket;
|
|
303
|
+
const isCacheable = req.path.startsWith("/static/docs/");
|
|
304
|
+
if (!isCacheable || !socket) {
|
|
305
|
+
next();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const reqPath = decodeURI(req.path.match(/\/static\/docs\/(.*)$/)[1]);
|
|
309
|
+
const realEnd = socket.end.bind(socket);
|
|
310
|
+
const realWrite = socket.write.bind(socket);
|
|
311
|
+
let writeToCache = true;
|
|
312
|
+
const chunks = [];
|
|
313
|
+
socket.write = (data, encoding, callback) => {
|
|
314
|
+
chunks.push(Buffer.from(data));
|
|
315
|
+
if (typeof encoding === "function") {
|
|
316
|
+
return realWrite(data, encoding);
|
|
317
|
+
}
|
|
318
|
+
return realWrite(data, encoding, callback);
|
|
319
|
+
};
|
|
320
|
+
socket.on("close", async (hadError) => {
|
|
321
|
+
const content = Buffer.concat(chunks);
|
|
322
|
+
const head = content.toString("utf8", 0, 12);
|
|
323
|
+
if (writeToCache && !hadError && head.match(/HTTP\/\d\.\d 200/)) {
|
|
324
|
+
await cache.set(reqPath, content);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
const cached = await cache.get(reqPath);
|
|
328
|
+
if (cached) {
|
|
329
|
+
writeToCache = false;
|
|
330
|
+
realEnd(cached);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
next();
|
|
334
|
+
});
|
|
335
|
+
return cacheMiddleware;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
class CacheInvalidationError extends errors.CustomErrorBase {
|
|
339
|
+
}
|
|
340
|
+
class TechDocsCache {
|
|
341
|
+
constructor({
|
|
342
|
+
cache,
|
|
343
|
+
logger,
|
|
344
|
+
readTimeout
|
|
345
|
+
}) {
|
|
346
|
+
this.cache = cache;
|
|
347
|
+
this.logger = logger;
|
|
348
|
+
this.readTimeout = readTimeout;
|
|
349
|
+
}
|
|
350
|
+
static fromConfig(config, { cache, logger }) {
|
|
351
|
+
const timeout = config.getOptionalNumber("techdocs.cache.readTimeout");
|
|
352
|
+
const readTimeout = timeout === void 0 ? 1e3 : timeout;
|
|
353
|
+
return new TechDocsCache({ cache, logger, readTimeout });
|
|
354
|
+
}
|
|
355
|
+
async get(path) {
|
|
356
|
+
try {
|
|
357
|
+
const response = await Promise.race([
|
|
358
|
+
this.cache.get(path),
|
|
359
|
+
new Promise((cancelAfter) => setTimeout(cancelAfter, this.readTimeout))
|
|
360
|
+
]);
|
|
361
|
+
if (response !== void 0) {
|
|
362
|
+
this.logger.debug(`Cache hit: ${path}`);
|
|
363
|
+
return Buffer.from(response, "base64");
|
|
364
|
+
}
|
|
365
|
+
this.logger.debug(`Cache miss: ${path}`);
|
|
366
|
+
return response;
|
|
367
|
+
} catch (e) {
|
|
368
|
+
errors.assertError(e);
|
|
369
|
+
this.logger.warn(`Error getting cache entry ${path}: ${e.message}`);
|
|
370
|
+
this.logger.debug(e.stack);
|
|
371
|
+
return void 0;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async set(path, data) {
|
|
375
|
+
this.logger.debug(`Writing cache entry for ${path}`);
|
|
376
|
+
this.cache.set(path, data.toString("base64")).catch((e) => this.logger.error("write error", e));
|
|
377
|
+
}
|
|
378
|
+
async invalidate(path) {
|
|
379
|
+
return this.cache.delete(path);
|
|
380
|
+
}
|
|
381
|
+
async invalidateMultiple(paths) {
|
|
382
|
+
const settled = await Promise.allSettled(paths.map((path) => this.cache.delete(path)));
|
|
383
|
+
const rejected = settled.filter((s) => s.status === "rejected");
|
|
384
|
+
if (rejected.length) {
|
|
385
|
+
throw new CacheInvalidationError("TechDocs cache invalidation error", rejected);
|
|
386
|
+
}
|
|
387
|
+
return settled;
|
|
240
388
|
}
|
|
241
389
|
}
|
|
242
390
|
|
|
@@ -244,19 +392,26 @@ function isOutOfTheBoxOption(opt) {
|
|
|
244
392
|
return opt.preparers !== void 0;
|
|
245
393
|
}
|
|
246
394
|
async function createRouter(options) {
|
|
247
|
-
const router =
|
|
248
|
-
const {publisher, config, logger, discovery} = options;
|
|
249
|
-
const catalogClient$1 = new catalogClient.CatalogClient({discoveryApi: discovery});
|
|
395
|
+
const router = router__default["default"]();
|
|
396
|
+
const { publisher, config, logger, discovery } = options;
|
|
397
|
+
const catalogClient$1 = new catalogClient.CatalogClient({ discoveryApi: discovery });
|
|
398
|
+
let cache;
|
|
399
|
+
const defaultTtl = config.getOptionalNumber("techdocs.cache.ttl");
|
|
400
|
+
if (options.cache && defaultTtl) {
|
|
401
|
+
const cacheClient = options.cache.getClient({ defaultTtl });
|
|
402
|
+
cache = TechDocsCache.fromConfig(config, { cache: cacheClient, logger });
|
|
403
|
+
}
|
|
250
404
|
const scmIntegrations = integration.ScmIntegrations.fromConfig(config);
|
|
251
405
|
const docsSynchronizer = new DocsSynchronizer({
|
|
252
406
|
publisher,
|
|
253
407
|
logger,
|
|
254
408
|
config,
|
|
255
|
-
scmIntegrations
|
|
409
|
+
scmIntegrations,
|
|
410
|
+
cache
|
|
256
411
|
});
|
|
257
412
|
router.get("/metadata/techdocs/:namespace/:kind/:name", async (req, res) => {
|
|
258
|
-
const {kind, namespace, name} = req.params;
|
|
259
|
-
const entityName = {kind, namespace, name};
|
|
413
|
+
const { kind, namespace, name } = req.params;
|
|
414
|
+
const entityName = { kind, namespace, name };
|
|
260
415
|
try {
|
|
261
416
|
const techdocsMetadata = await publisher.fetchTechDocsMetadata(entityName);
|
|
262
417
|
res.json(techdocsMetadata);
|
|
@@ -267,15 +422,15 @@ async function createRouter(options) {
|
|
|
267
422
|
});
|
|
268
423
|
router.get("/metadata/entity/:namespace/:kind/:name", async (req, res) => {
|
|
269
424
|
const catalogUrl = await discovery.getBaseUrl("catalog");
|
|
270
|
-
const {kind, namespace, name} = req.params;
|
|
271
|
-
const entityName = {kind, namespace, name};
|
|
425
|
+
const { kind, namespace, name } = req.params;
|
|
426
|
+
const entityName = { kind, namespace, name };
|
|
272
427
|
try {
|
|
273
428
|
const token = getBearerToken(req.headers.authorization);
|
|
274
|
-
const entity = await (await fetch__default[
|
|
275
|
-
headers: token ? {Authorization: `Bearer ${token}`} : {}
|
|
429
|
+
const entity = await (await fetch__default$1["default"](`${catalogUrl}/entities/by-name/${kind}/${namespace}/${name}`, {
|
|
430
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
276
431
|
})).json();
|
|
277
432
|
const locationMetadata = techdocsCommon.getLocationForEntity(entity, scmIntegrations);
|
|
278
|
-
res.json({...entity, locationMetadata});
|
|
433
|
+
res.json({ ...entity, locationMetadata });
|
|
279
434
|
} catch (err) {
|
|
280
435
|
logger.info(`Unable to get metadata for '${catalogModel.stringifyEntityRef(entityName)}' with error ${err}`);
|
|
281
436
|
throw new errors.NotFoundError(`Unable to get metadata for '${catalogModel.stringifyEntityRef(entityName)}'`, err);
|
|
@@ -283,9 +438,9 @@ async function createRouter(options) {
|
|
|
283
438
|
});
|
|
284
439
|
router.get("/sync/:namespace/:kind/:name", async (req, res) => {
|
|
285
440
|
var _a;
|
|
286
|
-
const {kind, namespace, name} = req.params;
|
|
441
|
+
const { kind, namespace, name } = req.params;
|
|
287
442
|
const token = getBearerToken(req.headers.authorization);
|
|
288
|
-
const entity = await catalogClient$1.getEntityByName({kind, namespace, name}, {token});
|
|
443
|
+
const entity = await catalogClient$1.getEntityByName({ kind, namespace, name }, { token });
|
|
289
444
|
if (!((_a = entity == null ? void 0 : entity.metadata) == null ? void 0 : _a.uid)) {
|
|
290
445
|
throw new errors.NotFoundError("Entity metadata UID missing");
|
|
291
446
|
}
|
|
@@ -297,11 +452,20 @@ async function createRouter(options) {
|
|
|
297
452
|
responseHandler = createEventStream(res);
|
|
298
453
|
}
|
|
299
454
|
if (config.getString("techdocs.builder") !== "local") {
|
|
300
|
-
|
|
455
|
+
if (cache) {
|
|
456
|
+
await docsSynchronizer.doCacheSync({
|
|
457
|
+
responseHandler,
|
|
458
|
+
discovery,
|
|
459
|
+
token,
|
|
460
|
+
entity
|
|
461
|
+
});
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
responseHandler.finish({ updated: false });
|
|
301
465
|
return;
|
|
302
466
|
}
|
|
303
467
|
if (isOutOfTheBoxOption(options)) {
|
|
304
|
-
const {preparers, generators} = options;
|
|
468
|
+
const { preparers, generators } = options;
|
|
305
469
|
await docsSynchronizer.doSync({
|
|
306
470
|
responseHandler,
|
|
307
471
|
entity,
|
|
@@ -312,6 +476,9 @@ async function createRouter(options) {
|
|
|
312
476
|
}
|
|
313
477
|
responseHandler.error(new Error("Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization."));
|
|
314
478
|
});
|
|
479
|
+
if (cache) {
|
|
480
|
+
router.use(createCacheMiddleware({ logger, cache }));
|
|
481
|
+
}
|
|
315
482
|
router.use("/static/docs", publisher.docsRouter());
|
|
316
483
|
return router;
|
|
317
484
|
}
|
|
@@ -359,39 +526,35 @@ function createHttpResponse(res) {
|
|
|
359
526
|
error: (e) => {
|
|
360
527
|
throw e;
|
|
361
528
|
},
|
|
362
|
-
finish: ({updated}) => {
|
|
529
|
+
finish: ({ updated }) => {
|
|
363
530
|
if (!updated) {
|
|
364
531
|
throw new errors.NotModifiedError();
|
|
365
532
|
}
|
|
366
|
-
res.status(201).json({message: "Docs updated or did not need updating"});
|
|
533
|
+
res.status(201).json({ message: "Docs updated or did not need updating" });
|
|
367
534
|
}
|
|
368
535
|
};
|
|
369
536
|
}
|
|
370
537
|
|
|
371
538
|
class DefaultTechDocsCollator {
|
|
372
|
-
constructor({
|
|
373
|
-
discovery,
|
|
374
|
-
locationTemplate,
|
|
375
|
-
logger,
|
|
376
|
-
catalogClient: catalogClient$1,
|
|
377
|
-
parallelismLimit = 10,
|
|
378
|
-
legacyPathCasing = false
|
|
379
|
-
}) {
|
|
539
|
+
constructor(options) {
|
|
380
540
|
this.type = "techdocs";
|
|
381
|
-
|
|
382
|
-
this.
|
|
383
|
-
this.
|
|
384
|
-
this.
|
|
385
|
-
this.
|
|
386
|
-
this.
|
|
541
|
+
var _a, _b;
|
|
542
|
+
this.discovery = options.discovery;
|
|
543
|
+
this.locationTemplate = options.locationTemplate || "/docs/:namespace/:kind/:name/:path";
|
|
544
|
+
this.logger = options.logger;
|
|
545
|
+
this.catalogClient = options.catalogClient || new catalogClient.CatalogClient({ discoveryApi: options.discovery });
|
|
546
|
+
this.parallelismLimit = (_a = options.parallelismLimit) != null ? _a : 10;
|
|
547
|
+
this.legacyPathCasing = (_b = options.legacyPathCasing) != null ? _b : false;
|
|
548
|
+
this.tokenManager = options.tokenManager;
|
|
387
549
|
}
|
|
388
550
|
static fromConfig(config, options) {
|
|
389
551
|
const legacyPathCasing = config.getOptionalBoolean("techdocs.legacyUseCaseSensitiveTripletPaths") || false;
|
|
390
|
-
return new DefaultTechDocsCollator({...options, legacyPathCasing});
|
|
552
|
+
return new DefaultTechDocsCollator({ ...options, legacyPathCasing });
|
|
391
553
|
}
|
|
392
554
|
async execute() {
|
|
393
|
-
const limit = pLimit__default[
|
|
555
|
+
const limit = pLimit__default["default"](this.parallelismLimit);
|
|
394
556
|
const techDocsBaseUrl = await this.discovery.getBaseUrl("techdocs");
|
|
557
|
+
const { token } = await this.tokenManager.getToken();
|
|
395
558
|
const entities = await this.catalogClient.getEntities({
|
|
396
559
|
fields: [
|
|
397
560
|
"kind",
|
|
@@ -404,7 +567,7 @@ class DefaultTechDocsCollator {
|
|
|
404
567
|
"spec.lifecycle",
|
|
405
568
|
"relations"
|
|
406
569
|
]
|
|
407
|
-
});
|
|
570
|
+
}, { token });
|
|
408
571
|
const docPromises = entities.items.filter((it) => {
|
|
409
572
|
var _a, _b;
|
|
410
573
|
return (_b = (_a = it.metadata) == null ? void 0 : _a.annotations) == null ? void 0 : _b["backstage.io/techdocs-ref"];
|
|
@@ -415,13 +578,17 @@ class DefaultTechDocsCollator {
|
|
|
415
578
|
name: entity.metadata.name
|
|
416
579
|
});
|
|
417
580
|
try {
|
|
418
|
-
const searchIndexResponse = await fetch__default[
|
|
581
|
+
const searchIndexResponse = await fetch__default$1["default"](DefaultTechDocsCollator.constructDocsIndexUrl(techDocsBaseUrl, entityInfo), {
|
|
582
|
+
headers: {
|
|
583
|
+
Authorization: `Bearer ${token}`
|
|
584
|
+
}
|
|
585
|
+
});
|
|
419
586
|
const searchIndex = await searchIndexResponse.json();
|
|
420
587
|
return searchIndex.docs.map((doc) => {
|
|
421
588
|
var _a, _b, _c, _d, _e, _f;
|
|
422
589
|
return {
|
|
423
|
-
title: unescape__default[
|
|
424
|
-
text: unescape__default[
|
|
590
|
+
title: unescape__default["default"](doc.title),
|
|
591
|
+
text: unescape__default["default"](doc.text || ""),
|
|
425
592
|
location: this.applyArgsToFormat(this.locationTemplate, {
|
|
426
593
|
...entityInfo,
|
|
427
594
|
path: doc.location
|
|
@@ -455,7 +622,7 @@ class DefaultTechDocsCollator {
|
|
|
455
622
|
}
|
|
456
623
|
static handleEntityInfoCasing(legacyPaths, entityInfo) {
|
|
457
624
|
return legacyPaths ? entityInfo : Object.entries(entityInfo).reduce((acc, [key, value]) => {
|
|
458
|
-
return {...acc, [key]: value.toLocaleLowerCase("en-US")};
|
|
625
|
+
return { ...acc, [key]: value.toLocaleLowerCase("en-US") };
|
|
459
626
|
}, {});
|
|
460
627
|
}
|
|
461
628
|
}
|
|
@@ -465,9 +632,7 @@ exports.createRouter = createRouter;
|
|
|
465
632
|
Object.keys(techdocsCommon).forEach(function (k) {
|
|
466
633
|
if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, {
|
|
467
634
|
enumerable: true,
|
|
468
|
-
get: function () {
|
|
469
|
-
return techdocsCommon[k];
|
|
470
|
-
}
|
|
635
|
+
get: function () { return techdocsCommon[k]; }
|
|
471
636
|
});
|
|
472
637
|
});
|
|
473
638
|
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":["../src/DocsBuilder/BuildMetadataStorage.ts","../src/DocsBuilder/builder.ts","../src/service/DocsSynchronizer.ts","../src/service/router.ts","../src/search/DefaultTechDocsCollator.ts"],"sourcesContent":["/*\n * Copyright 2020 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\n// Entity uid: unix timestamp\nconst lastUpdatedRecord = {} as Record<string, number>;\n\n/**\n * Store timestamps of the most recent TechDocs update of each Entity. This is\n * used to avoid checking for an update on each and every request to TechDocs.\n */\nexport class BuildMetadataStorage {\n private entityUid: string;\n private lastUpdatedRecord: Record<string, number>;\n\n constructor(entityUid: string) {\n this.entityUid = entityUid;\n this.lastUpdatedRecord = lastUpdatedRecord;\n }\n\n setLastUpdated(): void {\n this.lastUpdatedRecord[this.entityUid] = Date.now();\n }\n\n getLastUpdated(): number | undefined {\n return this.lastUpdatedRecord[this.entityUid];\n }\n}\n\n/**\n * Return false if a check for update has happened in last 60 seconds.\n */\nexport const shouldCheckForUpdate = (entityUid: string) => {\n const lastUpdated = new BuildMetadataStorage(entityUid).getLastUpdated();\n if (lastUpdated) {\n // The difference is in milliseconds\n if (Date.now() - lastUpdated < 60 * 1000) {\n return false;\n }\n }\n return true;\n};\n","/*\n * Copyright 2020 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 Entity,\n ENTITY_DEFAULT_NAMESPACE,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { assertError, isError } from '@backstage/errors';\nimport { ScmIntegrationRegistry } from '@backstage/integration';\nimport {\n GeneratorBase,\n GeneratorBuilder,\n getLocationForEntity,\n PreparerBase,\n PreparerBuilder,\n PublisherBase,\n UrlPreparer,\n} from '@backstage/techdocs-common';\nimport fs from 'fs-extra';\nimport os from 'os';\nimport path from 'path';\nimport { Writable } from 'stream';\nimport { Logger } from 'winston';\nimport { BuildMetadataStorage } from './BuildMetadataStorage';\n\ntype DocsBuilderArguments = {\n preparers: PreparerBuilder;\n generators: GeneratorBuilder;\n publisher: PublisherBase;\n entity: Entity;\n logger: Logger;\n config: Config;\n scmIntegrations: ScmIntegrationRegistry;\n logStream?: Writable;\n};\n\nexport class DocsBuilder {\n private preparer: PreparerBase;\n private generator: GeneratorBase;\n private publisher: PublisherBase;\n private entity: Entity;\n private logger: Logger;\n private config: Config;\n private scmIntegrations: ScmIntegrationRegistry;\n private logStream: Writable | undefined;\n\n constructor({\n preparers,\n generators,\n publisher,\n entity,\n logger,\n config,\n scmIntegrations,\n logStream,\n }: DocsBuilderArguments) {\n this.preparer = preparers.get(entity);\n this.generator = generators.get(entity);\n this.publisher = publisher;\n this.entity = entity;\n this.logger = logger;\n this.config = config;\n this.scmIntegrations = scmIntegrations;\n this.logStream = logStream;\n }\n\n /**\n * Build the docs and return whether they have been newly generated or have been cached\n * @returns true, if the docs have been built. false, if the cached docs are still up-to-date.\n */\n public async build(): Promise<boolean> {\n if (!this.entity.metadata.uid) {\n throw new Error(\n 'Trying to build documentation for entity not in software catalog',\n );\n }\n\n /**\n * Prepare (and cache check)\n */\n\n this.logger.info(\n `Step 1 of 3: Preparing docs for entity ${stringifyEntityRef(\n this.entity,\n )}`,\n );\n\n // If available, use the etag stored in techdocs_metadata.json to\n // check if docs are outdated and need to be regenerated.\n let storedEtag: string | undefined;\n if (await this.publisher.hasDocsBeenGenerated(this.entity)) {\n try {\n storedEtag = (\n await this.publisher.fetchTechDocsMetadata({\n namespace:\n this.entity.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE,\n kind: this.entity.kind,\n name: this.entity.metadata.name,\n })\n ).etag;\n } catch (err) {\n // Proceed with a fresh build\n this.logger.warn(\n `Unable to read techdocs_metadata.json, proceeding with fresh build, error ${err}.`,\n );\n }\n }\n\n let preparedDir: string;\n let newEtag: string;\n try {\n const preparerResponse = await this.preparer.prepare(this.entity, {\n etag: storedEtag,\n logger: this.logger,\n });\n\n preparedDir = preparerResponse.preparedDir;\n newEtag = preparerResponse.etag;\n } catch (err) {\n if (isError(err) && err.name === 'NotModifiedError') {\n // No need to prepare anymore since cache is valid.\n // Set last check happened to now\n new BuildMetadataStorage(this.entity.metadata.uid).setLastUpdated();\n this.logger.debug(\n `Docs for ${stringifyEntityRef(\n this.entity,\n )} are unmodified. Using cache, skipping generate and prepare`,\n );\n return false;\n }\n throw err;\n }\n\n this.logger.info(\n `Prepare step completed for entity ${stringifyEntityRef(\n this.entity,\n )}, stored at ${preparedDir}`,\n );\n\n /**\n * Generate\n */\n\n this.logger.info(\n `Step 2 of 3: Generating docs for entity ${stringifyEntityRef(\n this.entity,\n )}`,\n );\n\n const workingDir = this.config.getOptionalString(\n 'backend.workingDirectory',\n );\n const tmpdirPath = workingDir || os.tmpdir();\n // Fixes a problem with macOS returning a path that is a symlink\n const tmpdirResolvedPath = fs.realpathSync(tmpdirPath);\n const outputDir = await fs.mkdtemp(\n path.join(tmpdirResolvedPath, 'techdocs-tmp-'),\n );\n\n const parsedLocationAnnotation = getLocationForEntity(\n this.entity,\n this.scmIntegrations,\n );\n await this.generator.run({\n inputDir: preparedDir,\n outputDir,\n parsedLocationAnnotation,\n etag: newEtag,\n logger: this.logger,\n logStream: this.logStream,\n });\n\n // Remove Prepared directory since it is no longer needed.\n // Caveat: Can not remove prepared directory in case of git preparer since the\n // local git repository is used to get etag on subsequent requests.\n if (this.preparer instanceof UrlPreparer) {\n this.logger.debug(\n `Removing prepared directory ${preparedDir} since the site has been generated`,\n );\n try {\n // Not a blocker hence no need to await this.\n fs.remove(preparedDir);\n } catch (error) {\n assertError(error);\n this.logger.debug(`Error removing prepared directory ${error.message}`);\n }\n }\n\n /**\n * Publish\n */\n\n this.logger.info(\n `Step 3 of 3: Publishing docs for entity ${stringifyEntityRef(\n this.entity,\n )}`,\n );\n\n await this.publisher.publish({\n entity: this.entity,\n directory: outputDir,\n });\n\n try {\n // Not a blocker hence no need to await this.\n fs.remove(outputDir);\n this.logger.debug(\n `Removing generated directory ${outputDir} since the site has been published`,\n );\n } catch (error) {\n assertError(error);\n this.logger.debug(`Error removing generated directory ${error.message}`);\n }\n\n // Update the last check time for the entity\n new BuildMetadataStorage(this.entity.metadata.uid).setLastUpdated();\n\n return true;\n }\n}\n","/*\n * Copyright 2021 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 { Entity } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { assertError, NotFoundError } from '@backstage/errors';\nimport { ScmIntegrationRegistry } from '@backstage/integration';\nimport {\n GeneratorBuilder,\n PreparerBuilder,\n PublisherBase,\n} from '@backstage/techdocs-common';\nimport { PassThrough } from 'stream';\nimport * as winston from 'winston';\nimport { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder';\n\nexport type DocsSynchronizerSyncOpts = {\n log: (message: string) => void;\n error: (e: Error) => void;\n finish: (result: { updated: boolean }) => void;\n};\n\nexport class DocsSynchronizer {\n private readonly publisher: PublisherBase;\n private readonly logger: winston.Logger;\n private readonly config: Config;\n private readonly scmIntegrations: ScmIntegrationRegistry;\n\n constructor({\n publisher,\n logger,\n config,\n scmIntegrations,\n }: {\n publisher: PublisherBase;\n logger: winston.Logger;\n config: Config;\n scmIntegrations: ScmIntegrationRegistry;\n }) {\n this.config = config;\n this.logger = logger;\n this.publisher = publisher;\n this.scmIntegrations = scmIntegrations;\n }\n\n async doSync({\n responseHandler: { log, error, finish },\n entity,\n preparers,\n generators,\n }: {\n responseHandler: DocsSynchronizerSyncOpts;\n entity: Entity;\n preparers: PreparerBuilder;\n generators: GeneratorBuilder;\n }) {\n // create a new logger to log data to the caller\n const taskLogger = winston.createLogger({\n level: process.env.LOG_LEVEL || 'info',\n format: winston.format.combine(\n winston.format.colorize(),\n winston.format.timestamp(),\n winston.format.simple(),\n ),\n defaultMeta: {},\n });\n\n // create an in-memory stream to forward logs to the event-stream\n const logStream = new PassThrough();\n logStream.on('data', async data => {\n log(data.toString().trim());\n });\n\n taskLogger.add(new winston.transports.Stream({ stream: logStream }));\n\n // check if the last update check was too recent\n if (!shouldCheckForUpdate(entity.metadata.uid!)) {\n finish({ updated: false });\n return;\n }\n\n let foundDocs = false;\n\n try {\n const docsBuilder = new DocsBuilder({\n preparers,\n generators,\n publisher: this.publisher,\n logger: taskLogger,\n entity,\n config: this.config,\n scmIntegrations: this.scmIntegrations,\n logStream,\n });\n\n const updated = await docsBuilder.build();\n\n if (!updated) {\n finish({ updated: false });\n return;\n }\n } catch (e) {\n assertError(e);\n const msg = `Failed to build the docs page: ${e.message}`;\n taskLogger.error(msg);\n this.logger.error(msg, e);\n error(e);\n return;\n }\n\n // With a maximum of ~5 seconds wait, check if the files got published and if docs will be fetched\n // on the user's page. If not, respond with a message asking them to check back later.\n // The delay here is to make sure GCS/AWS/etc. registers newly uploaded files which is usually <1 second\n for (let attempt = 0; attempt < 5; attempt++) {\n if (await this.publisher.hasDocsBeenGenerated(entity)) {\n foundDocs = true;\n break;\n }\n await new Promise(r => setTimeout(r, 1000));\n }\n if (!foundDocs) {\n this.logger.error(\n 'Published files are taking longer to show up in storage. Something went wrong.',\n );\n error(\n new NotFoundError(\n 'Sorry! It took too long for the generated docs to show up in storage. Check back later.',\n ),\n );\n return;\n }\n\n finish({ updated: true });\n }\n}\n","/*\n * Copyright 2020 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 { PluginEndpointDiscovery } from '@backstage/backend-common';\nimport { CatalogClient } from '@backstage/catalog-client';\nimport { Entity, stringifyEntityRef } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { NotFoundError, NotModifiedError } from '@backstage/errors';\nimport {\n GeneratorBuilder,\n getLocationForEntity,\n PreparerBuilder,\n PublisherBase,\n} from '@backstage/techdocs-common';\nimport fetch from 'cross-fetch';\nimport express, { Response } from 'express';\nimport Router from 'express-promise-router';\nimport { Knex } from 'knex';\nimport { Logger } from 'winston';\nimport { ScmIntegrations } from '@backstage/integration';\nimport { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer';\n\n/**\n * All of the required dependencies for running TechDocs in the \"out-of-the-box\"\n * deployment configuration (prepare/generate/publish all in the Backend).\n */\ntype OutOfTheBoxDeploymentOptions = {\n preparers: PreparerBuilder;\n generators: GeneratorBuilder;\n publisher: PublisherBase;\n logger: Logger;\n discovery: PluginEndpointDiscovery;\n database?: Knex; // TODO: Make database required when we're implementing database stuff.\n config: Config;\n};\n\n/**\n * Required dependencies for running TechDocs in the \"recommended\" deployment\n * configuration (prepare/generate handled externally in CI/CD).\n */\ntype RecommendedDeploymentOptions = {\n publisher: PublisherBase;\n logger: Logger;\n discovery: PluginEndpointDiscovery;\n config: Config;\n};\n\n/**\n * One of the two deployment configurations must be provided.\n */\ntype RouterOptions =\n | RecommendedDeploymentOptions\n | OutOfTheBoxDeploymentOptions;\n\n/**\n * Typeguard to help createRouter() understand when we are in a \"recommended\"\n * deployment vs. when we are in an out-of-the-box deployment configuration.\n */\nfunction isOutOfTheBoxOption(\n opt: RouterOptions,\n): opt is OutOfTheBoxDeploymentOptions {\n return (opt as OutOfTheBoxDeploymentOptions).preparers !== undefined;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const router = Router();\n const { publisher, config, logger, discovery } = options;\n const catalogClient = new CatalogClient({ discoveryApi: discovery });\n const scmIntegrations = ScmIntegrations.fromConfig(config);\n const docsSynchronizer = new DocsSynchronizer({\n publisher,\n logger,\n config,\n scmIntegrations,\n });\n\n router.get('/metadata/techdocs/:namespace/:kind/:name', async (req, res) => {\n const { kind, namespace, name } = req.params;\n const entityName = { kind, namespace, name };\n\n try {\n const techdocsMetadata = await publisher.fetchTechDocsMetadata(\n entityName,\n );\n\n res.json(techdocsMetadata);\n } catch (err) {\n logger.info(\n `Unable to get metadata for '${stringifyEntityRef(\n entityName,\n )}' with error ${err}`,\n );\n throw new NotFoundError(\n `Unable to get metadata for '${stringifyEntityRef(entityName)}'`,\n err,\n );\n }\n });\n\n router.get('/metadata/entity/:namespace/:kind/:name', async (req, res) => {\n const catalogUrl = await discovery.getBaseUrl('catalog');\n\n const { kind, namespace, name } = req.params;\n const entityName = { kind, namespace, name };\n\n try {\n const token = getBearerToken(req.headers.authorization);\n // TODO: Consider using the catalog client here\n const entity = (await (\n await fetch(\n `${catalogUrl}/entities/by-name/${kind}/${namespace}/${name}`,\n {\n headers: token ? { Authorization: `Bearer ${token}` } : {},\n },\n )\n ).json()) as Entity;\n\n const locationMetadata = getLocationForEntity(entity, scmIntegrations);\n res.json({ ...entity, locationMetadata });\n } catch (err) {\n logger.info(\n `Unable to get metadata for '${stringifyEntityRef(\n entityName,\n )}' with error ${err}`,\n );\n throw new NotFoundError(\n `Unable to get metadata for '${stringifyEntityRef(entityName)}'`,\n err,\n );\n }\n });\n\n // Check if docs are the latest version and trigger rebuilds if not\n // Responds with an event-stream that closes after the build finished\n // Responds with an immediate success if rebuild not needed\n // If a build is required, responds with a success when finished\n router.get('/sync/:namespace/:kind/:name', async (req, res) => {\n const { kind, namespace, name } = req.params;\n const token = getBearerToken(req.headers.authorization);\n\n const entity = await catalogClient.getEntityByName(\n { kind, namespace, name },\n { token },\n );\n\n if (!entity?.metadata?.uid) {\n throw new NotFoundError('Entity metadata UID missing');\n }\n\n let responseHandler: DocsSynchronizerSyncOpts;\n if (req.header('accept') !== 'text/event-stream') {\n console.warn(\n \"The call to /sync/:namespace/:kind/:name wasn't done by an EventSource. This behavior is deprecated and will be removed soon. Make sure to update the @backstage/plugin-techdocs package in the frontend to the latest version.\",\n );\n responseHandler = createHttpResponse(res);\n } else {\n responseHandler = createEventStream(res);\n }\n\n // techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to 'local'\n // If set to 'external', it will assume that an external process (e.g. CI/CD pipeline\n // of the repository) is responsible for building and publishing documentation to the storage provider\n if (config.getString('techdocs.builder') !== 'local') {\n responseHandler.finish({ updated: false });\n return;\n }\n\n // Set the synchronization and build process if \"out-of-the-box\" configuration is provided.\n if (isOutOfTheBoxOption(options)) {\n const { preparers, generators } = options;\n\n await docsSynchronizer.doSync({\n responseHandler,\n entity,\n preparers,\n generators,\n });\n return;\n }\n\n responseHandler.error(\n new Error(\n \"Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization.\",\n ),\n );\n });\n\n // Route middleware which serves files from the storage set in the publisher.\n router.use('/static/docs', publisher.docsRouter());\n\n return router;\n}\n\nfunction getBearerToken(header?: string): string | undefined {\n return header?.match(/(?:Bearer)\\s+(\\S+)/i)?.[1];\n}\n\n/**\n * Create an event-stream response that emits the events 'log', 'error', and 'finish'.\n *\n * @param res the response to write the event-stream to\n * @returns A tuple of <log, error, finish> callbacks to emit messages. A call to 'error' or 'finish'\n * will close the event-stream.\n */\nexport function createEventStream(\n res: Response<any, any>,\n): DocsSynchronizerSyncOpts {\n // Mandatory headers and http status to keep connection open\n res.writeHead(200, {\n Connection: 'keep-alive',\n 'Cache-Control': 'no-cache',\n 'Content-Type': 'text/event-stream',\n });\n\n // client closes connection\n res.socket?.on('close', () => {\n res.end();\n });\n\n // write the event to the stream\n const send = (type: 'error' | 'finish' | 'log', data: any) => {\n res.write(`event: ${type}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n\n // res.flush() is only available with the compression middleware\n if (res.flush) {\n res.flush();\n }\n };\n\n return {\n log: data => {\n send('log', data);\n },\n\n error: e => {\n send('error', e.message);\n res.end();\n },\n\n finish: result => {\n send('finish', result);\n res.end();\n },\n };\n}\n\n/**\n * Create a HTTP response. This is used for the legacy non-event-stream implementation of the sync endpoint.\n *\n * @param res the response to write the event-stream to\n * @returns A tuple of <log, error, finish> callbacks to emit messages. A call to 'error' or 'finish'\n * will close the event-stream.\n */\nexport function createHttpResponse(\n res: Response<any, any>,\n): DocsSynchronizerSyncOpts {\n return {\n log: () => {},\n error: e => {\n throw e;\n },\n finish: ({ updated }) => {\n if (!updated) {\n throw new NotModifiedError();\n }\n\n res\n .status(201)\n .json({ message: 'Docs updated or did not need updating' });\n },\n };\n}\n","/*\n * Copyright 2021 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 { PluginEndpointDiscovery } from '@backstage/backend-common';\nimport { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model';\nimport { DocumentCollator } from '@backstage/search-common';\nimport fetch from 'cross-fetch';\nimport unescape from 'lodash/unescape';\nimport { Logger } from 'winston';\nimport pLimit from 'p-limit';\nimport { Config } from '@backstage/config';\nimport { CatalogApi, CatalogClient } from '@backstage/catalog-client';\nimport { TechDocsDocument } from '@backstage/techdocs-common';\n\ninterface MkSearchIndexDoc {\n title: string;\n text: string;\n location: string;\n}\n\nexport type TechDocsCollatorOptions = {\n discovery: PluginEndpointDiscovery;\n logger: Logger;\n locationTemplate?: string;\n catalogClient?: CatalogApi;\n parallelismLimit?: number;\n legacyPathCasing?: boolean;\n};\n\ntype EntityInfo = {\n name: string;\n namespace: string;\n kind: string;\n};\n\nexport class DefaultTechDocsCollator implements DocumentCollator {\n protected discovery: PluginEndpointDiscovery;\n protected locationTemplate: string;\n private readonly logger: Logger;\n private readonly catalogClient: CatalogApi;\n private readonly parallelismLimit: number;\n private readonly legacyPathCasing: boolean;\n public readonly type: string = 'techdocs';\n\n /**\n * @deprecated use static fromConfig method instead.\n */\n constructor({\n discovery,\n locationTemplate,\n logger,\n catalogClient,\n parallelismLimit = 10,\n legacyPathCasing = false,\n }: TechDocsCollatorOptions) {\n this.discovery = discovery;\n this.locationTemplate =\n locationTemplate || '/docs/:namespace/:kind/:name/:path';\n this.logger = logger;\n this.catalogClient =\n catalogClient || new CatalogClient({ discoveryApi: discovery });\n this.parallelismLimit = parallelismLimit;\n this.legacyPathCasing = legacyPathCasing;\n }\n\n static fromConfig(config: Config, options: TechDocsCollatorOptions) {\n const legacyPathCasing =\n config.getOptionalBoolean(\n 'techdocs.legacyUseCaseSensitiveTripletPaths',\n ) || false;\n return new DefaultTechDocsCollator({ ...options, legacyPathCasing });\n }\n\n async execute() {\n const limit = pLimit(this.parallelismLimit);\n const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs');\n const entities = await this.catalogClient.getEntities({\n fields: [\n 'kind',\n 'namespace',\n 'metadata.annotations',\n 'metadata.name',\n 'metadata.title',\n 'metadata.namespace',\n 'spec.type',\n 'spec.lifecycle',\n 'relations',\n ],\n });\n const docPromises = entities.items\n .filter(it => it.metadata?.annotations?.['backstage.io/techdocs-ref'])\n .map((entity: Entity) =>\n limit(async (): Promise<TechDocsDocument[]> => {\n const entityInfo = DefaultTechDocsCollator.handleEntityInfoCasing(\n this.legacyPathCasing,\n {\n kind: entity.kind,\n namespace: entity.metadata.namespace || 'default',\n name: entity.metadata.name,\n },\n );\n\n try {\n const searchIndexResponse = await fetch(\n DefaultTechDocsCollator.constructDocsIndexUrl(\n techDocsBaseUrl,\n entityInfo,\n ),\n );\n const searchIndex = await searchIndexResponse.json();\n\n return searchIndex.docs.map((doc: MkSearchIndexDoc) => ({\n title: unescape(doc.title),\n text: unescape(doc.text || ''),\n location: this.applyArgsToFormat(this.locationTemplate, {\n ...entityInfo,\n path: doc.location,\n }),\n path: doc.location,\n kind: entity.kind,\n namespace: entity.metadata.namespace || 'default',\n name: entity.metadata.name,\n entityTitle: entity.metadata.title,\n componentType: entity.spec?.type?.toString() || 'other',\n lifecycle: (entity.spec?.lifecycle as string) || '',\n owner:\n entity.relations?.find(r => r.type === RELATION_OWNED_BY)\n ?.target?.name || '',\n }));\n } catch (e) {\n this.logger.debug(\n `Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`,\n e,\n );\n return [];\n }\n }),\n );\n return (await Promise.all(docPromises)).flat();\n }\n\n protected applyArgsToFormat(\n format: string,\n args: Record<string, string>,\n ): string {\n let formatted = format;\n for (const [key, value] of Object.entries(args)) {\n formatted = formatted.replace(`:${key}`, value);\n }\n return formatted;\n }\n\n private static constructDocsIndexUrl(\n techDocsBaseUrl: string,\n entityInfo: { kind: string; namespace: string; name: string },\n ) {\n return `${techDocsBaseUrl}/static/docs/${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}/search/search_index.json`;\n }\n\n private static handleEntityInfoCasing(\n legacyPaths: boolean,\n entityInfo: EntityInfo,\n ): EntityInfo {\n return legacyPaths\n ? entityInfo\n : Object.entries(entityInfo).reduce((acc, [key, value]) => {\n return { ...acc, [key]: value.toLocaleLowerCase('en-US') };\n }, {} as EntityInfo);\n }\n}\n"],"names":["stringifyEntityRef","ENTITY_DEFAULT_NAMESPACE","isError","os","fs","path","getLocationForEntity","UrlPreparer","winston","PassThrough","NotFoundError","Router","catalogClient","CatalogClient","ScmIntegrations","fetch","NotModifiedError","pLimit","unescape","RELATION_OWNED_BY"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/DocsBuilder/BuildMetadataStorage.ts","../src/DocsBuilder/builder.ts","../src/service/DocsSynchronizer.ts","../src/cache/cacheMiddleware.ts","../src/cache/TechDocsCache.ts","../src/service/router.ts","../src/search/DefaultTechDocsCollator.ts"],"sourcesContent":["/*\n * Copyright 2020 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\n// Entity uid: unix timestamp\nconst lastUpdatedRecord = {} as Record<string, number>;\n\n/**\n * Store timestamps of the most recent TechDocs update of each Entity. This is\n * used to avoid checking for an update on each and every request to TechDocs.\n */\nexport class BuildMetadataStorage {\n private entityUid: string;\n private lastUpdatedRecord: Record<string, number>;\n\n constructor(entityUid: string) {\n this.entityUid = entityUid;\n this.lastUpdatedRecord = lastUpdatedRecord;\n }\n\n setLastUpdated(): void {\n this.lastUpdatedRecord[this.entityUid] = Date.now();\n }\n\n getLastUpdated(): number | undefined {\n return this.lastUpdatedRecord[this.entityUid];\n }\n}\n\n/**\n * Return false if a check for update has happened in last 60 seconds.\n */\nexport const shouldCheckForUpdate = (entityUid: string) => {\n const lastUpdated = new BuildMetadataStorage(entityUid).getLastUpdated();\n if (lastUpdated) {\n // The difference is in milliseconds\n if (Date.now() - lastUpdated < 60 * 1000) {\n return false;\n }\n }\n return true;\n};\n","/*\n * Copyright 2020 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 Entity,\n ENTITY_DEFAULT_NAMESPACE,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { assertError, isError } from '@backstage/errors';\nimport { ScmIntegrationRegistry } from '@backstage/integration';\nimport {\n GeneratorBase,\n GeneratorBuilder,\n getLocationForEntity,\n PreparerBase,\n PreparerBuilder,\n PublisherBase,\n UrlPreparer,\n} from '@backstage/techdocs-common';\nimport fs from 'fs-extra';\nimport os from 'os';\nimport path from 'path';\nimport { Writable } from 'stream';\nimport { Logger } from 'winston';\nimport { BuildMetadataStorage } from './BuildMetadataStorage';\nimport { TechDocsCache } from '../cache';\n\ntype DocsBuilderArguments = {\n preparers: PreparerBuilder;\n generators: GeneratorBuilder;\n publisher: PublisherBase;\n entity: Entity;\n logger: Logger;\n config: Config;\n scmIntegrations: ScmIntegrationRegistry;\n logStream?: Writable;\n cache?: TechDocsCache;\n};\n\nexport class DocsBuilder {\n private preparer: PreparerBase;\n private generator: GeneratorBase;\n private publisher: PublisherBase;\n private entity: Entity;\n private logger: Logger;\n private config: Config;\n private scmIntegrations: ScmIntegrationRegistry;\n private logStream: Writable | undefined;\n private cache?: TechDocsCache;\n\n constructor({\n preparers,\n generators,\n publisher,\n entity,\n logger,\n config,\n scmIntegrations,\n logStream,\n cache,\n }: DocsBuilderArguments) {\n this.preparer = preparers.get(entity);\n this.generator = generators.get(entity);\n this.publisher = publisher;\n this.entity = entity;\n this.logger = logger;\n this.config = config;\n this.scmIntegrations = scmIntegrations;\n this.logStream = logStream;\n this.cache = cache;\n }\n\n /**\n * Build the docs and return whether they have been newly generated or have been cached\n * @returns true, if the docs have been built. false, if the cached docs are still up-to-date.\n */\n public async build(): Promise<boolean> {\n if (!this.entity.metadata.uid) {\n throw new Error(\n 'Trying to build documentation for entity not in software catalog',\n );\n }\n\n /**\n * Prepare (and cache check)\n */\n\n this.logger.info(\n `Step 1 of 3: Preparing docs for entity ${stringifyEntityRef(\n this.entity,\n )}`,\n );\n\n // If available, use the etag stored in techdocs_metadata.json to\n // check if docs are outdated and need to be regenerated.\n let storedEtag: string | undefined;\n if (await this.publisher.hasDocsBeenGenerated(this.entity)) {\n try {\n storedEtag = (\n await this.publisher.fetchTechDocsMetadata({\n namespace:\n this.entity.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE,\n kind: this.entity.kind,\n name: this.entity.metadata.name,\n })\n ).etag;\n } catch (err) {\n // Proceed with a fresh build\n this.logger.warn(\n `Unable to read techdocs_metadata.json, proceeding with fresh build, error ${err}.`,\n );\n }\n }\n\n let preparedDir: string;\n let newEtag: string;\n try {\n const preparerResponse = await this.preparer.prepare(this.entity, {\n etag: storedEtag,\n logger: this.logger,\n });\n\n preparedDir = preparerResponse.preparedDir;\n newEtag = preparerResponse.etag;\n } catch (err) {\n if (isError(err) && err.name === 'NotModifiedError') {\n // No need to prepare anymore since cache is valid.\n // Set last check happened to now\n new BuildMetadataStorage(this.entity.metadata.uid).setLastUpdated();\n this.logger.debug(\n `Docs for ${stringifyEntityRef(\n this.entity,\n )} are unmodified. Using cache, skipping generate and prepare`,\n );\n return false;\n }\n throw err;\n }\n\n this.logger.info(\n `Prepare step completed for entity ${stringifyEntityRef(\n this.entity,\n )}, stored at ${preparedDir}`,\n );\n\n /**\n * Generate\n */\n\n this.logger.info(\n `Step 2 of 3: Generating docs for entity ${stringifyEntityRef(\n this.entity,\n )}`,\n );\n\n const workingDir = this.config.getOptionalString(\n 'backend.workingDirectory',\n );\n const tmpdirPath = workingDir || os.tmpdir();\n // Fixes a problem with macOS returning a path that is a symlink\n const tmpdirResolvedPath = fs.realpathSync(tmpdirPath);\n const outputDir = await fs.mkdtemp(\n path.join(tmpdirResolvedPath, 'techdocs-tmp-'),\n );\n\n const parsedLocationAnnotation = getLocationForEntity(\n this.entity,\n this.scmIntegrations,\n );\n await this.generator.run({\n inputDir: preparedDir,\n outputDir,\n parsedLocationAnnotation,\n etag: newEtag,\n logger: this.logger,\n logStream: this.logStream,\n });\n\n // Remove Prepared directory since it is no longer needed.\n // Caveat: Can not remove prepared directory in case of git preparer since the\n // local git repository is used to get etag on subsequent requests.\n if (this.preparer instanceof UrlPreparer) {\n this.logger.debug(\n `Removing prepared directory ${preparedDir} since the site has been generated`,\n );\n try {\n // Not a blocker hence no need to await this.\n fs.remove(preparedDir);\n } catch (error) {\n assertError(error);\n this.logger.debug(`Error removing prepared directory ${error.message}`);\n }\n }\n\n /**\n * Publish\n */\n\n this.logger.info(\n `Step 3 of 3: Publishing docs for entity ${stringifyEntityRef(\n this.entity,\n )}`,\n );\n\n const published = await this.publisher.publish({\n entity: this.entity,\n directory: outputDir,\n });\n\n // Invalidate the cache for any published objects.\n if (this.cache && published && published?.objects?.length) {\n this.logger.debug(\n `Invalidating ${published.objects.length} cache objects`,\n );\n await this.cache.invalidateMultiple(published.objects);\n }\n\n try {\n // Not a blocker hence no need to await this.\n fs.remove(outputDir);\n this.logger.debug(\n `Removing generated directory ${outputDir} since the site has been published`,\n );\n } catch (error) {\n assertError(error);\n this.logger.debug(`Error removing generated directory ${error.message}`);\n }\n\n // Update the last check time for the entity\n new BuildMetadataStorage(this.entity.metadata.uid).setLastUpdated();\n\n return true;\n }\n}\n","/*\n * Copyright 2021 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 { PluginEndpointDiscovery } from '@backstage/backend-common';\nimport { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { assertError, NotFoundError } from '@backstage/errors';\nimport { ScmIntegrationRegistry } from '@backstage/integration';\nimport {\n GeneratorBuilder,\n PreparerBuilder,\n PublisherBase,\n} from '@backstage/techdocs-common';\nimport fetch from 'cross-fetch';\nimport { PassThrough } from 'stream';\nimport * as winston from 'winston';\nimport { TechDocsCache } from '../cache';\nimport {\n BuildMetadataStorage,\n DocsBuilder,\n shouldCheckForUpdate,\n} from '../DocsBuilder';\n\nexport type DocsSynchronizerSyncOpts = {\n log: (message: string) => void;\n error: (e: Error) => void;\n finish: (result: { updated: boolean }) => void;\n};\n\nexport class DocsSynchronizer {\n private readonly publisher: PublisherBase;\n private readonly logger: winston.Logger;\n private readonly config: Config;\n private readonly scmIntegrations: ScmIntegrationRegistry;\n private readonly cache: TechDocsCache | undefined;\n\n constructor({\n publisher,\n logger,\n config,\n scmIntegrations,\n cache,\n }: {\n publisher: PublisherBase;\n logger: winston.Logger;\n config: Config;\n scmIntegrations: ScmIntegrationRegistry;\n cache: TechDocsCache | undefined;\n }) {\n this.config = config;\n this.logger = logger;\n this.publisher = publisher;\n this.scmIntegrations = scmIntegrations;\n this.cache = cache;\n }\n\n async doSync({\n responseHandler: { log, error, finish },\n entity,\n preparers,\n generators,\n }: {\n responseHandler: DocsSynchronizerSyncOpts;\n entity: Entity;\n preparers: PreparerBuilder;\n generators: GeneratorBuilder;\n }) {\n // create a new logger to log data to the caller\n const taskLogger = winston.createLogger({\n level: process.env.LOG_LEVEL || 'info',\n format: winston.format.combine(\n winston.format.colorize(),\n winston.format.timestamp(),\n winston.format.simple(),\n ),\n defaultMeta: {},\n });\n\n // create an in-memory stream to forward logs to the event-stream\n const logStream = new PassThrough();\n logStream.on('data', async data => {\n log(data.toString().trim());\n });\n\n taskLogger.add(new winston.transports.Stream({ stream: logStream }));\n\n // check if the last update check was too recent\n if (!shouldCheckForUpdate(entity.metadata.uid!)) {\n finish({ updated: false });\n return;\n }\n\n let foundDocs = false;\n\n try {\n const docsBuilder = new DocsBuilder({\n preparers,\n generators,\n publisher: this.publisher,\n logger: taskLogger,\n entity,\n config: this.config,\n scmIntegrations: this.scmIntegrations,\n logStream,\n cache: this.cache,\n });\n\n const updated = await docsBuilder.build();\n\n if (!updated) {\n finish({ updated: false });\n return;\n }\n } catch (e) {\n assertError(e);\n const msg = `Failed to build the docs page: ${e.message}`;\n taskLogger.error(msg);\n this.logger.error(msg, e);\n error(e);\n return;\n }\n\n // With a maximum of ~5 seconds wait, check if the files got published and if docs will be fetched\n // on the user's page. If not, respond with a message asking them to check back later.\n // The delay here is to make sure GCS/AWS/etc. registers newly uploaded files which is usually <1 second\n for (let attempt = 0; attempt < 5; attempt++) {\n if (await this.publisher.hasDocsBeenGenerated(entity)) {\n foundDocs = true;\n break;\n }\n await new Promise(r => setTimeout(r, 1000));\n }\n if (!foundDocs) {\n this.logger.error(\n 'Published files are taking longer to show up in storage. Something went wrong.',\n );\n error(\n new NotFoundError(\n 'Sorry! It took too long for the generated docs to show up in storage. Check back later.',\n ),\n );\n return;\n }\n\n finish({ updated: true });\n }\n\n async doCacheSync({\n responseHandler: { finish },\n discovery,\n token,\n entity,\n }: {\n responseHandler: DocsSynchronizerSyncOpts;\n discovery: PluginEndpointDiscovery;\n token: string | undefined;\n entity: Entity;\n }) {\n // Check if the last update check was too recent.\n if (!shouldCheckForUpdate(entity.metadata.uid!) || !this.cache) {\n finish({ updated: false });\n return;\n }\n\n // Fetch techdocs_metadata.json from the publisher and from cache.\n const baseUrl = await discovery.getBaseUrl('techdocs');\n const namespace = entity.metadata?.namespace || ENTITY_DEFAULT_NAMESPACE;\n const kind = entity.kind;\n const name = entity.metadata.name;\n const legacyPathCasing =\n this.config.getOptionalBoolean(\n 'techdocs.legacyUseCaseSensitiveTripletPaths',\n ) || false;\n const tripletPath = `${namespace}/${kind}/${name}`;\n const entityTripletPath = `${\n legacyPathCasing ? tripletPath : tripletPath.toLocaleLowerCase('en-US')\n }`;\n try {\n const [sourceMetadata, cachedMetadata] = await Promise.all([\n this.publisher.fetchTechDocsMetadata({ namespace, kind, name }),\n fetch(\n `${baseUrl}/static/docs/${entityTripletPath}/techdocs_metadata.json`,\n {\n headers: token ? { Authorization: `Bearer ${token}` } : {},\n },\n ).then(\n f =>\n f.json().catch(() => undefined) as ReturnType<\n PublisherBase['fetchTechDocsMetadata']\n >,\n ),\n ]);\n\n // If build timestamps differ, merge their files[] lists and invalidate all objects.\n if (sourceMetadata.build_timestamp !== cachedMetadata.build_timestamp) {\n const files = [\n ...new Set([\n ...(sourceMetadata.files || []),\n ...(cachedMetadata.files || []),\n ]),\n ].map(f => `${entityTripletPath}/${f}`);\n await this.cache.invalidateMultiple(files);\n finish({ updated: true });\n } else {\n finish({ updated: false });\n }\n } catch (e) {\n assertError(e);\n // In case of error, log and allow the user to go about their business.\n this.logger.error(\n `Error syncing cache for ${entityTripletPath}: ${e.message}`,\n );\n finish({ updated: false });\n } finally {\n // Update the last check time for the entity\n new BuildMetadataStorage(entity.metadata.uid!).setLastUpdated();\n }\n }\n}\n","/*\n * Copyright 2021 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 { Router } from 'express';\nimport router from 'express-promise-router';\nimport { Logger } from 'winston';\nimport { TechDocsCache } from './TechDocsCache';\n\ntype CacheMiddlewareOptions = {\n cache: TechDocsCache;\n logger: Logger;\n};\n\ntype ErrorCallback = (err?: Error) => void;\n\nexport const createCacheMiddleware = ({\n cache,\n}: CacheMiddlewareOptions): Router => {\n const cacheMiddleware = router();\n\n // Middleware that, through socket monkey patching, captures responses as\n // they're sent over /static/docs/* and caches them. Subsequent requests are\n // loaded from cache. Cache key is the object's path (after `/static/docs/`).\n cacheMiddleware.use(async (req, res, next) => {\n const socket = res.socket;\n const isCacheable = req.path.startsWith('/static/docs/');\n\n // Continue early if this is non-cacheable, or there's no socket.\n if (!isCacheable || !socket) {\n next();\n return;\n }\n\n // Make concrete references to these things.\n const reqPath = decodeURI(req.path.match(/\\/static\\/docs\\/(.*)$/)![1]);\n const realEnd = socket.end.bind(socket);\n const realWrite = socket.write.bind(socket);\n let writeToCache = true;\n const chunks: Buffer[] = [];\n\n // Monkey-patch the response's socket to keep track of chunks as they are\n // written over the wire.\n socket.write = (\n data: string | Uint8Array,\n encoding?: BufferEncoding | ErrorCallback,\n callback?: ErrorCallback,\n ) => {\n chunks.push(Buffer.from(data));\n if (typeof encoding === 'function') {\n return realWrite(data, encoding);\n }\n return realWrite(data, encoding, callback);\n };\n\n // When a socket is closed, if there were no errors and the data written\n // over the socket should be cached, cache it!\n socket.on('close', async hadError => {\n const content = Buffer.concat(chunks);\n const head = content.toString('utf8', 0, 12);\n if (writeToCache && !hadError && head.match(/HTTP\\/\\d\\.\\d 200/)) {\n await cache.set(reqPath, content);\n }\n });\n\n // Attempt to retrieve data from the cache.\n const cached = await cache.get(reqPath);\n\n // If there is a cache hit, write it out on the socket, ensure we don't re-\n // cache the data, and prevent going back to canonical storage by never\n // calling next().\n if (cached) {\n writeToCache = false;\n realEnd(cached);\n return;\n }\n\n // No data retrieved from cache: allow retrieval from canonical storage.\n next();\n });\n\n return cacheMiddleware;\n};\n","/*\n * Copyright 2021 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 { CacheClient } from '@backstage/backend-common';\nimport { assertError, CustomErrorBase } from '@backstage/errors';\nimport { Config } from '@backstage/config';\nimport { Logger } from 'winston';\n\nexport class CacheInvalidationError extends CustomErrorBase {}\n\nexport class TechDocsCache {\n protected readonly cache: CacheClient;\n protected readonly logger: Logger;\n protected readonly readTimeout: number;\n\n private constructor({\n cache,\n logger,\n readTimeout,\n }: {\n cache: CacheClient;\n logger: Logger;\n readTimeout: number;\n }) {\n this.cache = cache;\n this.logger = logger;\n this.readTimeout = readTimeout;\n }\n\n static fromConfig(\n config: Config,\n { cache, logger }: { cache: CacheClient; logger: Logger },\n ) {\n const timeout = config.getOptionalNumber('techdocs.cache.readTimeout');\n const readTimeout = timeout === undefined ? 1000 : timeout;\n return new TechDocsCache({ cache, logger, readTimeout });\n }\n\n async get(path: string): Promise<Buffer | undefined> {\n try {\n // Promise.race ensures we don't hang the client for long if the cache is\n // temporarily unreachable.\n const response = (await Promise.race([\n this.cache.get(path),\n new Promise(cancelAfter => setTimeout(cancelAfter, this.readTimeout)),\n ])) as string | undefined;\n\n if (response !== undefined) {\n this.logger.debug(`Cache hit: ${path}`);\n return Buffer.from(response, 'base64');\n }\n\n this.logger.debug(`Cache miss: ${path}`);\n return response;\n } catch (e) {\n assertError(e);\n this.logger.warn(`Error getting cache entry ${path}: ${e.message}`);\n this.logger.debug(e.stack);\n return undefined;\n }\n }\n\n async set(path: string, data: Buffer): Promise<void> {\n this.logger.debug(`Writing cache entry for ${path}`);\n this.cache\n .set(path, data.toString('base64'))\n .catch(e => this.logger.error('write error', e));\n }\n\n async invalidate(path: string): Promise<void> {\n return this.cache.delete(path);\n }\n\n async invalidateMultiple(\n paths: string[],\n ): Promise<PromiseSettledResult<void>[]> {\n const settled = await Promise.allSettled(\n paths.map(path => this.cache.delete(path)),\n );\n const rejected = settled.filter(\n s => s.status === 'rejected',\n ) as PromiseRejectedResult[];\n\n if (rejected.length) {\n throw new CacheInvalidationError(\n 'TechDocs cache invalidation error',\n rejected,\n );\n }\n\n return settled;\n }\n}\n","/*\n * Copyright 2020 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 PluginEndpointDiscovery,\n PluginCacheManager,\n} from '@backstage/backend-common';\nimport { CatalogClient } from '@backstage/catalog-client';\nimport { Entity, stringifyEntityRef } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { NotFoundError, NotModifiedError } from '@backstage/errors';\nimport {\n GeneratorBuilder,\n getLocationForEntity,\n PreparerBuilder,\n PublisherBase,\n} from '@backstage/techdocs-common';\nimport fetch from 'node-fetch';\nimport express, { Response } from 'express';\nimport Router from 'express-promise-router';\nimport { Knex } from 'knex';\nimport { Logger } from 'winston';\nimport { ScmIntegrations } from '@backstage/integration';\nimport { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer';\nimport { createCacheMiddleware, TechDocsCache } from '../cache';\n\n/**\n * All of the required dependencies for running TechDocs in the \"out-of-the-box\"\n * deployment configuration (prepare/generate/publish all in the Backend).\n */\ntype OutOfTheBoxDeploymentOptions = {\n preparers: PreparerBuilder;\n generators: GeneratorBuilder;\n publisher: PublisherBase;\n logger: Logger;\n discovery: PluginEndpointDiscovery;\n database?: Knex; // TODO: Make database required when we're implementing database stuff.\n config: Config;\n cache?: PluginCacheManager;\n};\n\n/**\n * Required dependencies for running TechDocs in the \"recommended\" deployment\n * configuration (prepare/generate handled externally in CI/CD).\n */\ntype RecommendedDeploymentOptions = {\n publisher: PublisherBase;\n logger: Logger;\n discovery: PluginEndpointDiscovery;\n config: Config;\n cache?: PluginCacheManager;\n};\n\n/**\n * One of the two deployment configurations must be provided.\n */\ntype RouterOptions =\n | RecommendedDeploymentOptions\n | OutOfTheBoxDeploymentOptions;\n\n/**\n * Typeguard to help createRouter() understand when we are in a \"recommended\"\n * deployment vs. when we are in an out-of-the-box deployment configuration.\n */\nfunction isOutOfTheBoxOption(\n opt: RouterOptions,\n): opt is OutOfTheBoxDeploymentOptions {\n return (opt as OutOfTheBoxDeploymentOptions).preparers !== undefined;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const router = Router();\n const { publisher, config, logger, discovery } = options;\n const catalogClient = new CatalogClient({ discoveryApi: discovery });\n\n // Set up a cache client if configured.\n let cache: TechDocsCache | undefined;\n const defaultTtl = config.getOptionalNumber('techdocs.cache.ttl');\n if (options.cache && defaultTtl) {\n const cacheClient = options.cache.getClient({ defaultTtl });\n cache = TechDocsCache.fromConfig(config, { cache: cacheClient, logger });\n }\n\n const scmIntegrations = ScmIntegrations.fromConfig(config);\n const docsSynchronizer = new DocsSynchronizer({\n publisher,\n logger,\n config,\n scmIntegrations,\n cache,\n });\n\n router.get('/metadata/techdocs/:namespace/:kind/:name', async (req, res) => {\n const { kind, namespace, name } = req.params;\n const entityName = { kind, namespace, name };\n\n try {\n const techdocsMetadata = await publisher.fetchTechDocsMetadata(\n entityName,\n );\n\n res.json(techdocsMetadata);\n } catch (err) {\n logger.info(\n `Unable to get metadata for '${stringifyEntityRef(\n entityName,\n )}' with error ${err}`,\n );\n throw new NotFoundError(\n `Unable to get metadata for '${stringifyEntityRef(entityName)}'`,\n err,\n );\n }\n });\n\n router.get('/metadata/entity/:namespace/:kind/:name', async (req, res) => {\n const catalogUrl = await discovery.getBaseUrl('catalog');\n\n const { kind, namespace, name } = req.params;\n const entityName = { kind, namespace, name };\n\n try {\n const token = getBearerToken(req.headers.authorization);\n // TODO: Consider using the catalog client here\n const entity = (await (\n await fetch(\n `${catalogUrl}/entities/by-name/${kind}/${namespace}/${name}`,\n {\n headers: token ? { Authorization: `Bearer ${token}` } : {},\n },\n )\n ).json()) as Entity;\n\n const locationMetadata = getLocationForEntity(entity, scmIntegrations);\n res.json({ ...entity, locationMetadata });\n } catch (err) {\n logger.info(\n `Unable to get metadata for '${stringifyEntityRef(\n entityName,\n )}' with error ${err}`,\n );\n throw new NotFoundError(\n `Unable to get metadata for '${stringifyEntityRef(entityName)}'`,\n err,\n );\n }\n });\n\n // Check if docs are the latest version and trigger rebuilds if not\n // Responds with an event-stream that closes after the build finished\n // Responds with an immediate success if rebuild not needed\n // If a build is required, responds with a success when finished\n router.get('/sync/:namespace/:kind/:name', async (req, res) => {\n const { kind, namespace, name } = req.params;\n const token = getBearerToken(req.headers.authorization);\n\n const entity = await catalogClient.getEntityByName(\n { kind, namespace, name },\n { token },\n );\n\n if (!entity?.metadata?.uid) {\n throw new NotFoundError('Entity metadata UID missing');\n }\n\n let responseHandler: DocsSynchronizerSyncOpts;\n if (req.header('accept') !== 'text/event-stream') {\n console.warn(\n \"The call to /sync/:namespace/:kind/:name wasn't done by an EventSource. This behavior is deprecated and will be removed soon. Make sure to update the @backstage/plugin-techdocs package in the frontend to the latest version.\",\n );\n responseHandler = createHttpResponse(res);\n } else {\n responseHandler = createEventStream(res);\n }\n\n // techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to 'local'\n // If set to 'external', it will assume that an external process (e.g. CI/CD pipeline\n // of the repository) is responsible for building and publishing documentation to the storage provider\n if (config.getString('techdocs.builder') !== 'local') {\n // However, if caching is enabled, take the opportunity to check and\n // invalidate stale cache entries.\n if (cache) {\n await docsSynchronizer.doCacheSync({\n responseHandler,\n discovery,\n token,\n entity,\n });\n return;\n }\n responseHandler.finish({ updated: false });\n return;\n }\n\n // Set the synchronization and build process if \"out-of-the-box\" configuration is provided.\n if (isOutOfTheBoxOption(options)) {\n const { preparers, generators } = options;\n\n await docsSynchronizer.doSync({\n responseHandler,\n entity,\n preparers,\n generators,\n });\n return;\n }\n\n responseHandler.error(\n new Error(\n \"Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization.\",\n ),\n );\n });\n\n // If a cache manager was provided, attach the cache middleware.\n if (cache) {\n router.use(createCacheMiddleware({ logger, cache }));\n }\n\n // Route middleware which serves files from the storage set in the publisher.\n router.use('/static/docs', publisher.docsRouter());\n\n return router;\n}\n\nfunction getBearerToken(header?: string): string | undefined {\n return header?.match(/(?:Bearer)\\s+(\\S+)/i)?.[1];\n}\n\n/**\n * Create an event-stream response that emits the events 'log', 'error', and 'finish'.\n *\n * @param res the response to write the event-stream to\n * @returns A tuple of <log, error, finish> callbacks to emit messages. A call to 'error' or 'finish'\n * will close the event-stream.\n */\nexport function createEventStream(\n res: Response<any, any>,\n): DocsSynchronizerSyncOpts {\n // Mandatory headers and http status to keep connection open\n res.writeHead(200, {\n Connection: 'keep-alive',\n 'Cache-Control': 'no-cache',\n 'Content-Type': 'text/event-stream',\n });\n\n // client closes connection\n res.socket?.on('close', () => {\n res.end();\n });\n\n // write the event to the stream\n const send = (type: 'error' | 'finish' | 'log', data: any) => {\n res.write(`event: ${type}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n\n // res.flush() is only available with the compression middleware\n if (res.flush) {\n res.flush();\n }\n };\n\n return {\n log: data => {\n send('log', data);\n },\n\n error: e => {\n send('error', e.message);\n res.end();\n },\n\n finish: result => {\n send('finish', result);\n res.end();\n },\n };\n}\n\n/**\n * Create a HTTP response. This is used for the legacy non-event-stream implementation of the sync endpoint.\n *\n * @param res the response to write the event-stream to\n * @returns A tuple of <log, error, finish> callbacks to emit messages. A call to 'error' or 'finish'\n * will close the event-stream.\n */\nexport function createHttpResponse(\n res: Response<any, any>,\n): DocsSynchronizerSyncOpts {\n return {\n log: () => {},\n error: e => {\n throw e;\n },\n finish: ({ updated }) => {\n if (!updated) {\n throw new NotModifiedError();\n }\n\n res\n .status(201)\n .json({ message: 'Docs updated or did not need updating' });\n },\n };\n}\n","/*\n * Copyright 2021 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 PluginEndpointDiscovery,\n TokenManager,\n} from '@backstage/backend-common';\nimport { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model';\nimport { DocumentCollator } from '@backstage/search-common';\nimport fetch from 'node-fetch';\nimport unescape from 'lodash/unescape';\nimport { Logger } from 'winston';\nimport pLimit from 'p-limit';\nimport { Config } from '@backstage/config';\nimport { CatalogApi, CatalogClient } from '@backstage/catalog-client';\nimport { TechDocsDocument } from '@backstage/techdocs-common';\n\ninterface MkSearchIndexDoc {\n title: string;\n text: string;\n location: string;\n}\n\nexport type TechDocsCollatorOptions = {\n discovery: PluginEndpointDiscovery;\n logger: Logger;\n tokenManager: TokenManager;\n locationTemplate?: string;\n catalogClient?: CatalogApi;\n parallelismLimit?: number;\n legacyPathCasing?: boolean;\n};\n\ntype EntityInfo = {\n name: string;\n namespace: string;\n kind: string;\n};\n\nexport class DefaultTechDocsCollator implements DocumentCollator {\n protected discovery: PluginEndpointDiscovery;\n protected locationTemplate: string;\n private readonly logger: Logger;\n private readonly catalogClient: CatalogApi;\n private readonly tokenManager: TokenManager;\n private readonly parallelismLimit: number;\n private readonly legacyPathCasing: boolean;\n public readonly type: string = 'techdocs';\n\n /**\n * @deprecated use static fromConfig method instead.\n */\n constructor(options: TechDocsCollatorOptions) {\n this.discovery = options.discovery;\n this.locationTemplate =\n options.locationTemplate || '/docs/:namespace/:kind/:name/:path';\n this.logger = options.logger;\n this.catalogClient =\n options.catalogClient ||\n new CatalogClient({ discoveryApi: options.discovery });\n this.parallelismLimit = options.parallelismLimit ?? 10;\n this.legacyPathCasing = options.legacyPathCasing ?? false;\n this.tokenManager = options.tokenManager;\n }\n\n static fromConfig(config: Config, options: TechDocsCollatorOptions) {\n const legacyPathCasing =\n config.getOptionalBoolean(\n 'techdocs.legacyUseCaseSensitiveTripletPaths',\n ) || false;\n return new DefaultTechDocsCollator({ ...options, legacyPathCasing });\n }\n\n async execute() {\n const limit = pLimit(this.parallelismLimit);\n const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs');\n const { token } = await this.tokenManager.getToken();\n const entities = await this.catalogClient.getEntities(\n {\n fields: [\n 'kind',\n 'namespace',\n 'metadata.annotations',\n 'metadata.name',\n 'metadata.title',\n 'metadata.namespace',\n 'spec.type',\n 'spec.lifecycle',\n 'relations',\n ],\n },\n { token },\n );\n const docPromises = entities.items\n .filter(it => it.metadata?.annotations?.['backstage.io/techdocs-ref'])\n .map((entity: Entity) =>\n limit(async (): Promise<TechDocsDocument[]> => {\n const entityInfo = DefaultTechDocsCollator.handleEntityInfoCasing(\n this.legacyPathCasing,\n {\n kind: entity.kind,\n namespace: entity.metadata.namespace || 'default',\n name: entity.metadata.name,\n },\n );\n\n try {\n const searchIndexResponse = await fetch(\n DefaultTechDocsCollator.constructDocsIndexUrl(\n techDocsBaseUrl,\n entityInfo,\n ),\n {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n },\n );\n const searchIndex = await searchIndexResponse.json();\n\n return searchIndex.docs.map((doc: MkSearchIndexDoc) => ({\n title: unescape(doc.title),\n text: unescape(doc.text || ''),\n location: this.applyArgsToFormat(this.locationTemplate, {\n ...entityInfo,\n path: doc.location,\n }),\n path: doc.location,\n kind: entity.kind,\n namespace: entity.metadata.namespace || 'default',\n name: entity.metadata.name,\n entityTitle: entity.metadata.title,\n componentType: entity.spec?.type?.toString() || 'other',\n lifecycle: (entity.spec?.lifecycle as string) || '',\n owner:\n entity.relations?.find(r => r.type === RELATION_OWNED_BY)\n ?.target?.name || '',\n }));\n } catch (e) {\n this.logger.debug(\n `Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`,\n e,\n );\n return [];\n }\n }),\n );\n return (await Promise.all(docPromises)).flat();\n }\n\n protected applyArgsToFormat(\n format: string,\n args: Record<string, string>,\n ): string {\n let formatted = format;\n for (const [key, value] of Object.entries(args)) {\n formatted = formatted.replace(`:${key}`, value);\n }\n return formatted;\n }\n\n private static constructDocsIndexUrl(\n techDocsBaseUrl: string,\n entityInfo: { kind: string; namespace: string; name: string },\n ) {\n return `${techDocsBaseUrl}/static/docs/${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}/search/search_index.json`;\n }\n\n private static handleEntityInfoCasing(\n legacyPaths: boolean,\n entityInfo: EntityInfo,\n ): EntityInfo {\n return legacyPaths\n ? entityInfo\n : Object.entries(entityInfo).reduce((acc, [key, value]) => {\n return { ...acc, [key]: value.toLocaleLowerCase('en-US') };\n }, {} as EntityInfo);\n }\n}\n"],"names":["stringifyEntityRef","ENTITY_DEFAULT_NAMESPACE","isError","os","fs","path","getLocationForEntity","UrlPreparer","winston","PassThrough","NotFoundError","fetch","router","CustomErrorBase","Router","catalogClient","CatalogClient","ScmIntegrations","NotModifiedError","pLimit","unescape","RELATION_OWNED_BY"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
|
1
|
+
import { PluginEndpointDiscovery, PluginCacheManager, TokenManager } from '@backstage/backend-common';
|
|
2
2
|
import { Config } from '@backstage/config';
|
|
3
3
|
import { PreparerBuilder, GeneratorBuilder, PublisherBase, TechDocsDocument } from '@backstage/techdocs-common';
|
|
4
4
|
export * from '@backstage/techdocs-common';
|
|
@@ -21,6 +21,7 @@ declare type OutOfTheBoxDeploymentOptions = {
|
|
|
21
21
|
discovery: PluginEndpointDiscovery;
|
|
22
22
|
database?: Knex;
|
|
23
23
|
config: Config;
|
|
24
|
+
cache?: PluginCacheManager;
|
|
24
25
|
};
|
|
25
26
|
/**
|
|
26
27
|
* Required dependencies for running TechDocs in the "recommended" deployment
|
|
@@ -31,6 +32,7 @@ declare type RecommendedDeploymentOptions = {
|
|
|
31
32
|
logger: Logger;
|
|
32
33
|
discovery: PluginEndpointDiscovery;
|
|
33
34
|
config: Config;
|
|
35
|
+
cache?: PluginCacheManager;
|
|
34
36
|
};
|
|
35
37
|
/**
|
|
36
38
|
* One of the two deployment configurations must be provided.
|
|
@@ -41,6 +43,7 @@ declare function createRouter(options: RouterOptions): Promise<express.Router>;
|
|
|
41
43
|
declare type TechDocsCollatorOptions = {
|
|
42
44
|
discovery: PluginEndpointDiscovery;
|
|
43
45
|
logger: Logger;
|
|
46
|
+
tokenManager: TokenManager;
|
|
44
47
|
locationTemplate?: string;
|
|
45
48
|
catalogClient?: CatalogApi;
|
|
46
49
|
parallelismLimit?: number;
|
|
@@ -51,13 +54,14 @@ declare class DefaultTechDocsCollator implements DocumentCollator {
|
|
|
51
54
|
protected locationTemplate: string;
|
|
52
55
|
private readonly logger;
|
|
53
56
|
private readonly catalogClient;
|
|
57
|
+
private readonly tokenManager;
|
|
54
58
|
private readonly parallelismLimit;
|
|
55
59
|
private readonly legacyPathCasing;
|
|
56
60
|
readonly type: string;
|
|
57
61
|
/**
|
|
58
62
|
* @deprecated use static fromConfig method instead.
|
|
59
63
|
*/
|
|
60
|
-
constructor(
|
|
64
|
+
constructor(options: TechDocsCollatorOptions);
|
|
61
65
|
static fromConfig(config: Config, options: TechDocsCollatorOptions): DefaultTechDocsCollator;
|
|
62
66
|
execute(): Promise<TechDocsDocument[]>;
|
|
63
67
|
protected applyArgsToFormat(format: string, args: Record<string, string>): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-techdocs-backend",
|
|
3
3
|
"description": "The Backstage backend plugin that renders technical documentation for your components",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.2",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"clean": "backstage-cli clean"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@backstage/backend-common": "^0.
|
|
35
|
-
"@backstage/catalog-client": "^0.5.
|
|
34
|
+
"@backstage/backend-common": "^0.10.1",
|
|
35
|
+
"@backstage/catalog-client": "^0.5.3",
|
|
36
36
|
"@backstage/catalog-model": "^0.9.7",
|
|
37
37
|
"@backstage/config": "^0.1.11",
|
|
38
38
|
"@backstage/errors": "^0.1.5",
|
|
39
|
-
"@backstage/integration": "^0.
|
|
39
|
+
"@backstage/integration": "^0.7.0",
|
|
40
40
|
"@backstage/search-common": "^0.2.1",
|
|
41
|
-
"@backstage/techdocs-common": "^0.
|
|
41
|
+
"@backstage/techdocs-common": "^0.11.2",
|
|
42
42
|
"@types/express": "^4.17.6",
|
|
43
43
|
"cross-fetch": "^3.0.6",
|
|
44
44
|
"dockerode": "^3.3.1",
|
|
@@ -47,12 +47,13 @@
|
|
|
47
47
|
"fs-extra": "9.1.0",
|
|
48
48
|
"knex": "^0.95.1",
|
|
49
49
|
"lodash": "^4.17.21",
|
|
50
|
+
"node-fetch": "^2.6.1",
|
|
50
51
|
"p-limit": "^3.1.0",
|
|
51
52
|
"winston": "^3.2.1"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"@backstage/cli": "^0.
|
|
55
|
-
"@backstage/test-utils": "^0.1
|
|
55
|
+
"@backstage/cli": "^0.10.4",
|
|
56
|
+
"@backstage/test-utils": "^0.2.1",
|
|
56
57
|
"@types/dockerode": "^3.3.0",
|
|
57
58
|
"msw": "^0.35.0",
|
|
58
59
|
"supertest": "^6.1.3"
|
|
@@ -62,5 +63,5 @@
|
|
|
62
63
|
"config.d.ts"
|
|
63
64
|
],
|
|
64
65
|
"configSchema": "config.d.ts",
|
|
65
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "4b2a8ed96ff427735c872a72c1864321ef698436"
|
|
66
67
|
}
|