@backstage/plugin-techdocs-backend 0.13.4 → 0.14.1

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 CHANGED
@@ -1,5 +1,78 @@
1
1
  # @backstage/plugin-techdocs-backend
2
2
 
3
+ ## 0.14.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 6537a601c7: Added a new interface that allows for customization of when to build techdocs
8
+ - 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client
9
+ - 022507c860: A `DefaultTechDocsCollatorFactory`, which works with the new stream-based
10
+ search indexing subsystem, is now available. The `DefaultTechDocsCollator` will
11
+ continue to be available for those unable to upgrade to the stream-based
12
+ `@backstage/search-backend-node` (and related packages), however it is now
13
+ marked as deprecated and will be removed in a future version.
14
+
15
+ To upgrade this plugin and the search indexing subsystem in one go, check
16
+ [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta)
17
+ for necessary changes to your search backend plugin configuration.
18
+
19
+ - 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`.
20
+ - Updated dependencies
21
+ - @backstage/catalog-model@0.12.0
22
+ - @backstage/catalog-client@0.8.0
23
+ - @backstage/backend-common@0.12.0
24
+ - @backstage/plugin-catalog-common@0.2.0
25
+ - @backstage/integration@0.8.0
26
+ - @backstage/search-common@0.3.0
27
+ - @backstage/techdocs-common@0.11.11
28
+
29
+ ## 0.14.0
30
+
31
+ ### Minor Changes
32
+
33
+ - a925ba8385: BREAKING: constructor based initialization of DefaultTechDocsCollator now deprecated. Use static fromConfig method instead.
34
+
35
+ ```diff
36
+ indexBuilder.addCollator({
37
+ defaultRefreshIntervalSeconds: 600,
38
+ - collator: new DefaultTechDocsCollator({
39
+ + collator: DefaultTechDocsCollator.fromConfig(config, {
40
+ discovery,
41
+ logger,
42
+ tokenManager,
43
+ }),
44
+ });
45
+ ```
46
+
47
+ Note: in an upcoming release, TechDocs backend's /sync/:namespace/:kind/:name endpoint will only respond to text/event-stream-based requests. Update any custom code at your organization accordingly.
48
+
49
+ ### Patch Changes
50
+
51
+ - 91eb01b5cf: Optimize DefaultTechDocsCollator get entities.
52
+ - 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field
53
+ - Updated dependencies
54
+ - @backstage/backend-common@0.11.0
55
+ - @backstage/catalog-model@0.11.0
56
+ - @backstage/catalog-client@0.7.2
57
+ - @backstage/techdocs-common@0.11.10
58
+ - @backstage/integration@0.7.5
59
+
60
+ ## 0.13.5
61
+
62
+ ### Patch Changes
63
+
64
+ - Fix for the previous release with missing type declarations.
65
+ - Updated dependencies
66
+ - @backstage/backend-common@0.10.9
67
+ - @backstage/catalog-client@0.7.1
68
+ - @backstage/catalog-model@0.10.1
69
+ - @backstage/config@0.1.15
70
+ - @backstage/errors@0.2.2
71
+ - @backstage/integration@0.7.4
72
+ - @backstage/search-common@0.2.4
73
+ - @backstage/techdocs-common@0.11.9
74
+ - @backstage/plugin-catalog-common@0.1.4
75
+
3
76
  ## 0.13.4
4
77
 
5
78
  ### Patch Changes
package/dist/index.cjs.js CHANGED
@@ -14,9 +14,9 @@ var winston = require('winston');
14
14
  var fs = require('fs-extra');
15
15
  var os = require('os');
16
16
  var path = require('path');
17
+ var pluginCatalogCommon = require('@backstage/plugin-catalog-common');
17
18
  var unescape = require('lodash/unescape');
18
19
  var pLimit = require('p-limit');
19
- var pluginCatalogCommon = require('@backstage/plugin-catalog-common');
20
20
 
21
21
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
22
22
 
@@ -393,13 +393,13 @@ class CachedEntityLoader {
393
393
  this.catalog = catalog;
394
394
  this.cache = cache;
395
395
  }
396
- async load(entityName, token) {
397
- const cacheKey = this.getCacheKey(entityName, token);
396
+ async load(entityRef, token) {
397
+ const cacheKey = this.getCacheKey(entityRef, token);
398
398
  let result = await this.getFromCache(cacheKey);
399
399
  if (result) {
400
400
  return result;
401
401
  }
402
- result = await this.catalog.getEntityByName(entityName, { token });
402
+ result = await this.catalog.getEntityByRef(entityRef, { token });
403
403
  if (result) {
404
404
  this.cache.set(cacheKey, result, { ttl: 5e3 });
405
405
  }
@@ -420,13 +420,27 @@ class CachedEntityLoader {
420
420
  }
421
421
  }
422
422
 
423
+ class DefaultDocsBuildStrategy {
424
+ constructor(config) {
425
+ this.config = config;
426
+ }
427
+ static fromConfig(config) {
428
+ return new DefaultDocsBuildStrategy(config);
429
+ }
430
+ async shouldBuild(_) {
431
+ return this.config.getString("techdocs.builder") === "local";
432
+ }
433
+ }
434
+
423
435
  function isOutOfTheBoxOption(opt) {
424
436
  return opt.preparers !== void 0;
425
437
  }
426
438
  async function createRouter(options) {
439
+ var _a;
427
440
  const router = router__default["default"]();
428
441
  const { publisher, config, logger, discovery } = options;
429
442
  const catalogClient$1 = new catalogClient.CatalogClient({ discoveryApi: discovery });
443
+ const docsBuildStrategy = (_a = options.docsBuildStrategy) != null ? _a : DefaultDocsBuildStrategy.fromConfig(config);
430
444
  const entityLoader = new CachedEntityLoader({
431
445
  catalog: catalogClient$1,
432
446
  cache: options.cache.getClient()
@@ -478,11 +492,11 @@ async function createRouter(options) {
478
492
  }
479
493
  });
480
494
  router.get("/sync/:namespace/:kind/:name", async (req, res) => {
481
- var _a;
495
+ var _a2;
482
496
  const { kind, namespace, name } = req.params;
483
497
  const token = getBearerToken(req.headers.authorization);
484
498
  const entity = await entityLoader.load({ kind, namespace, name }, token);
485
- if (!((_a = entity == null ? void 0 : entity.metadata) == null ? void 0 : _a.uid)) {
499
+ if (!((_a2 = entity == null ? void 0 : entity.metadata) == null ? void 0 : _a2.uid)) {
486
500
  throw new errors.NotFoundError("Entity metadata UID missing");
487
501
  }
488
502
  let responseHandler;
@@ -492,7 +506,8 @@ async function createRouter(options) {
492
506
  } else {
493
507
  responseHandler = createEventStream(res);
494
508
  }
495
- if (config.getString("techdocs.builder") !== "local") {
509
+ const shouldBuild = await docsBuildStrategy.shouldBuild({ entity });
510
+ if (!shouldBuild) {
496
511
  if (cache) {
497
512
  await docsSynchronizer.doCacheSync({
498
513
  responseHandler,
@@ -515,7 +530,7 @@ async function createRouter(options) {
515
530
  });
516
531
  return;
517
532
  }
518
- responseHandler.error(new Error("Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization."));
533
+ responseHandler.error(new Error("Invalid configuration. docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization."));
519
534
  });
520
535
  if (config.getOptionalBoolean("permission.enabled")) {
521
536
  router.use("/static/docs/:namespace/:kind/:name", async (req, _res, next) => {
@@ -588,7 +603,7 @@ function createHttpResponse(res) {
588
603
  };
589
604
  }
590
605
 
591
- class DefaultTechDocsCollator {
606
+ class DefaultTechDocsCollatorFactory {
592
607
  constructor(options) {
593
608
  this.type = "techdocs";
594
609
  this.visibilityPermission = pluginCatalogCommon.catalogEntityReadPermission;
@@ -603,13 +618,137 @@ class DefaultTechDocsCollator {
603
618
  }
604
619
  static fromConfig(config, options) {
605
620
  const legacyPathCasing = config.getOptionalBoolean("techdocs.legacyUseCaseSensitiveTripletPaths") || false;
606
- return new DefaultTechDocsCollator({ ...options, legacyPathCasing });
621
+ return new DefaultTechDocsCollatorFactory({ ...options, legacyPathCasing });
607
622
  }
608
- async execute() {
623
+ async getCollator() {
624
+ return stream.Readable.from(this.execute());
625
+ }
626
+ async *execute() {
609
627
  const limit = pLimit__default["default"](this.parallelismLimit);
610
628
  const techDocsBaseUrl = await this.discovery.getBaseUrl("techdocs");
611
629
  const { token } = await this.tokenManager.getToken();
612
- const entities = await this.catalogClient.getEntities({
630
+ let entitiesRetrieved = 0;
631
+ let moreEntitiesToGet = true;
632
+ const batchSize = this.parallelismLimit * 50;
633
+ while (moreEntitiesToGet) {
634
+ const entities = (await this.catalogClient.getEntities({
635
+ filter: {
636
+ "metadata.annotations.backstage.io/techdocs-ref": catalogClient.CATALOG_FILTER_EXISTS
637
+ },
638
+ fields: [
639
+ "kind",
640
+ "namespace",
641
+ "metadata.annotations",
642
+ "metadata.name",
643
+ "metadata.title",
644
+ "metadata.namespace",
645
+ "spec.type",
646
+ "spec.lifecycle",
647
+ "relations"
648
+ ],
649
+ limit: batchSize,
650
+ offset: entitiesRetrieved
651
+ }, { token })).items;
652
+ moreEntitiesToGet = entities.length === batchSize;
653
+ entitiesRetrieved += entities.length;
654
+ const docPromises = entities.filter((it) => {
655
+ var _a, _b;
656
+ return (_b = (_a = it.metadata) == null ? void 0 : _a.annotations) == null ? void 0 : _b["backstage.io/techdocs-ref"];
657
+ }).map((entity) => limit(async () => {
658
+ const entityInfo = DefaultTechDocsCollatorFactory.handleEntityInfoCasing(this.legacyPathCasing, {
659
+ kind: entity.kind,
660
+ namespace: entity.metadata.namespace || "default",
661
+ name: entity.metadata.name
662
+ });
663
+ try {
664
+ const searchIndexResponse = await fetch__default["default"](DefaultTechDocsCollatorFactory.constructDocsIndexUrl(techDocsBaseUrl, entityInfo), {
665
+ headers: {
666
+ Authorization: `Bearer ${token}`
667
+ }
668
+ });
669
+ const searchIndex = await searchIndexResponse.json();
670
+ return searchIndex.docs.map((doc) => {
671
+ var _a, _b, _c;
672
+ return {
673
+ title: unescape__default["default"](doc.title),
674
+ text: unescape__default["default"](doc.text || ""),
675
+ location: this.applyArgsToFormat(this.locationTemplate || "/docs/:namespace/:kind/:name/:path", {
676
+ ...entityInfo,
677
+ path: doc.location
678
+ }),
679
+ path: doc.location,
680
+ ...entityInfo,
681
+ entityTitle: entity.metadata.title,
682
+ componentType: ((_b = (_a = entity.spec) == null ? void 0 : _a.type) == null ? void 0 : _b.toString()) || "other",
683
+ lifecycle: ((_c = entity.spec) == null ? void 0 : _c.lifecycle) || "",
684
+ owner: getSimpleEntityOwnerString$1(entity),
685
+ authorization: {
686
+ resourceRef: catalogModel.stringifyEntityRef(entity)
687
+ }
688
+ };
689
+ });
690
+ } catch (e) {
691
+ this.logger.debug(`Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`, e);
692
+ return [];
693
+ }
694
+ }));
695
+ yield* (await Promise.all(docPromises)).flat();
696
+ }
697
+ }
698
+ applyArgsToFormat(format, args) {
699
+ let formatted = format;
700
+ for (const [key, value] of Object.entries(args)) {
701
+ formatted = formatted.replace(`:${key}`, value);
702
+ }
703
+ return formatted;
704
+ }
705
+ static constructDocsIndexUrl(techDocsBaseUrl, entityInfo) {
706
+ return `${techDocsBaseUrl}/static/docs/${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}/search/search_index.json`;
707
+ }
708
+ static handleEntityInfoCasing(legacyPaths, entityInfo) {
709
+ return legacyPaths ? entityInfo : Object.entries(entityInfo).reduce((acc, [key, value]) => {
710
+ return { ...acc, [key]: value.toLocaleLowerCase("en-US") };
711
+ }, {});
712
+ }
713
+ }
714
+ function getSimpleEntityOwnerString$1(entity) {
715
+ if (entity.relations) {
716
+ const owner = entity.relations.find((r) => r.type === catalogModel.RELATION_OWNED_BY);
717
+ if (owner) {
718
+ const { name } = catalogModel.parseEntityRef(owner.targetRef);
719
+ return name;
720
+ }
721
+ }
722
+ return "";
723
+ }
724
+
725
+ class DefaultTechDocsCollator {
726
+ constructor(legacyPathCasing, options) {
727
+ this.legacyPathCasing = legacyPathCasing;
728
+ this.options = options;
729
+ this.type = "techdocs";
730
+ this.visibilityPermission = pluginCatalogCommon.catalogEntityReadPermission;
731
+ }
732
+ static fromConfig(config, options) {
733
+ const legacyPathCasing = config.getOptionalBoolean("techdocs.legacyUseCaseSensitiveTripletPaths") || false;
734
+ return new DefaultTechDocsCollator(legacyPathCasing, options);
735
+ }
736
+ async execute() {
737
+ const {
738
+ parallelismLimit,
739
+ discovery,
740
+ tokenManager,
741
+ catalogClient: catalogClient$1,
742
+ locationTemplate,
743
+ logger
744
+ } = this.options;
745
+ const limit = pLimit__default["default"](parallelismLimit != null ? parallelismLimit : 10);
746
+ const techDocsBaseUrl = await discovery.getBaseUrl("techdocs");
747
+ const { token } = await tokenManager.getToken();
748
+ const entities = await (catalogClient$1 != null ? catalogClient$1 : new catalogClient.CatalogClient({ discoveryApi: discovery })).getEntities({
749
+ filter: {
750
+ "metadata.annotations.backstage.io/techdocs-ref": catalogClient.CATALOG_FILTER_EXISTS
751
+ },
613
752
  fields: [
614
753
  "kind",
615
754
  "namespace",
@@ -622,11 +761,9 @@ class DefaultTechDocsCollator {
622
761
  "relations"
623
762
  ]
624
763
  }, { token });
625
- const docPromises = entities.items.filter((it) => {
626
- var _a, _b;
627
- return (_b = (_a = it.metadata) == null ? void 0 : _a.annotations) == null ? void 0 : _b["backstage.io/techdocs-ref"];
628
- }).map((entity) => limit(async () => {
629
- const entityInfo = DefaultTechDocsCollator.handleEntityInfoCasing(this.legacyPathCasing, {
764
+ const docPromises = entities.items.map((entity) => limit(async () => {
765
+ var _a;
766
+ const entityInfo = DefaultTechDocsCollator.handleEntityInfoCasing((_a = this.legacyPathCasing) != null ? _a : false, {
630
767
  kind: entity.kind,
631
768
  namespace: entity.metadata.namespace || "default",
632
769
  name: entity.metadata.name
@@ -639,27 +776,27 @@ class DefaultTechDocsCollator {
639
776
  });
640
777
  const searchIndex = await searchIndexResponse.json();
641
778
  return searchIndex.docs.map((doc) => {
642
- var _a, _b, _c, _d, _e, _f;
779
+ var _a2, _b, _c;
643
780
  return {
644
781
  title: unescape__default["default"](doc.title),
645
782
  text: unescape__default["default"](doc.text || ""),
646
- location: this.applyArgsToFormat(this.locationTemplate, {
783
+ location: this.applyArgsToFormat(locationTemplate || "/docs/:namespace/:kind/:name/:path", {
647
784
  ...entityInfo,
648
785
  path: doc.location
649
786
  }),
650
787
  path: doc.location,
651
788
  ...entityInfo,
652
789
  entityTitle: entity.metadata.title,
653
- componentType: ((_b = (_a = entity.spec) == null ? void 0 : _a.type) == null ? void 0 : _b.toString()) || "other",
790
+ componentType: ((_b = (_a2 = entity.spec) == null ? void 0 : _a2.type) == null ? void 0 : _b.toString()) || "other",
654
791
  lifecycle: ((_c = entity.spec) == null ? void 0 : _c.lifecycle) || "",
655
- owner: ((_f = (_e = (_d = entity.relations) == null ? void 0 : _d.find((r) => r.type === catalogModel.RELATION_OWNED_BY)) == null ? void 0 : _e.target) == null ? void 0 : _f.name) || "",
792
+ owner: getSimpleEntityOwnerString(entity),
656
793
  authorization: {
657
794
  resourceRef: catalogModel.stringifyEntityRef(entity)
658
795
  }
659
796
  };
660
797
  });
661
798
  } catch (e) {
662
- this.logger.debug(`Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`, e);
799
+ logger.debug(`Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`, e);
663
800
  return [];
664
801
  }
665
802
  }));
@@ -681,8 +818,19 @@ class DefaultTechDocsCollator {
681
818
  }, {});
682
819
  }
683
820
  }
821
+ function getSimpleEntityOwnerString(entity) {
822
+ if (entity.relations) {
823
+ const owner = entity.relations.find((r) => r.type === catalogModel.RELATION_OWNED_BY);
824
+ if (owner) {
825
+ const { name } = catalogModel.parseEntityRef(owner.targetRef);
826
+ return name;
827
+ }
828
+ }
829
+ return "";
830
+ }
684
831
 
685
832
  exports.DefaultTechDocsCollator = DefaultTechDocsCollator;
833
+ exports.DefaultTechDocsCollatorFactory = DefaultTechDocsCollatorFactory;
686
834
  exports.createRouter = createRouter;
687
835
  Object.keys(techdocsCommon).forEach(function (k) {
688
836
  if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, {
@@ -1 +1 @@
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/CachedEntityLoader.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 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: this.entity.metadata.namespace ?? 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, 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 'node-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 || 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 2022 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 { CatalogClient } from '@backstage/catalog-client';\nimport { CacheClient } from '@backstage/backend-common';\nimport {\n Entity,\n EntityName,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\n\nexport type CachedEntityLoaderOptions = {\n catalog: CatalogClient;\n cache: CacheClient;\n};\n\nexport class CachedEntityLoader {\n private readonly catalog: CatalogClient;\n private readonly cache: CacheClient;\n private readonly readTimeout = 1000;\n\n constructor({ catalog, cache }: CachedEntityLoaderOptions) {\n this.catalog = catalog;\n this.cache = cache;\n }\n\n async load(\n entityName: EntityName,\n token: string | undefined,\n ): Promise<Entity | undefined> {\n const cacheKey = this.getCacheKey(entityName, token);\n let result = await this.getFromCache(cacheKey);\n\n if (result) {\n return result;\n }\n\n result = await this.catalog.getEntityByName(entityName, { token });\n\n if (result) {\n this.cache.set(cacheKey, result, { ttl: 5000 });\n }\n\n return result;\n }\n\n private async getFromCache(key: string): Promise<Entity | undefined> {\n // Promise.race ensures we don't hang the client for long if the cache is\n // temporarily unreachable.\n return (await Promise.race([\n this.cache.get(key),\n new Promise(cancelAfter => setTimeout(cancelAfter, this.readTimeout)),\n ])) as Entity | undefined;\n }\n\n private getCacheKey(\n entityName: EntityName,\n token: string | undefined,\n ): string {\n const key = ['catalog', stringifyEntityRef(entityName)];\n\n if (token) {\n key.push(token);\n }\n\n return key.join(':');\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 { 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 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';\nimport { CachedEntityLoader } from './CachedEntityLoader';\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 */\nexport type 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 */\nexport type 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 */\nexport type 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 // Entities are cached to optimize the /static/docs request path, which can be called many times\n // when loading a single techdocs page.\n const entityLoader = new CachedEntityLoader({\n catalog: catalogClient,\n cache: options.cache.getClient(),\n });\n\n // Set up a cache client if configured.\n let cache: TechDocsCache | undefined;\n const defaultTtl = config.getOptionalNumber('techdocs.cache.ttl');\n if (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 const token = getBearerToken(req.headers.authorization);\n\n // Verify that the related entity exists and the current user has permission to view it.\n const entity = await entityLoader.load(entityName, token);\n\n if (!entity) {\n throw new NotFoundError(\n `Unable to get metadata for '${stringifyEntityRef(entityName)}'`,\n );\n }\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 { kind, namespace, name } = req.params;\n const entityName = { kind, namespace, name };\n const token = getBearerToken(req.headers.authorization);\n\n const entity = await entityLoader.load(entityName, token);\n\n if (!entity) {\n throw new NotFoundError(\n `Unable to get metadata for '${stringifyEntityRef(entityName)}'`,\n );\n }\n\n try {\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 entityLoader.load({ kind, namespace, name }, token);\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 // Ensures that the related entity exists and the current user has permission to view it.\n if (config.getOptionalBoolean('permission.enabled')) {\n router.use(\n '/static/docs/:namespace/:kind/:name',\n async (req, _res, next) => {\n const { kind, namespace, name } = req.params;\n const entityName = { kind, namespace, name };\n const token = getBearerToken(req.headers.authorization);\n\n const entity = await entityLoader.load(entityName, token);\n\n if (!entity) {\n throw new NotFoundError(\n `Entity not found for ${stringifyEntityRef(entityName)}`,\n );\n }\n\n next();\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 {\n Entity,\n RELATION_OWNED_BY,\n stringifyEntityRef,\n} 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 { catalogEntityReadPermission } from '@backstage/plugin-catalog-common';\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 public readonly visibilityPermission = catalogEntityReadPermission;\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 ...entityInfo,\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 authorization: {\n resourceRef: stringifyEntityRef(entity),\n },\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","DEFAULT_NAMESPACE","isError","os","fs","path","getLocationForEntity","UrlPreparer","winston","PassThrough","NotFoundError","fetch","router","CustomErrorBase","Router","catalogClient","CatalogClient","ScmIntegrations","NotModifiedError","catalogEntityReadPermission","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/CachedEntityLoader.ts","../src/service/DocsBuildStrategy.ts","../src/service/router.ts","../src/search/DefaultTechDocsCollatorFactory.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 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: this.entity.metadata.namespace ?? 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, 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 'node-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 || 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 2022 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 { CatalogClient } from '@backstage/catalog-client';\nimport { CacheClient } from '@backstage/backend-common';\nimport {\n Entity,\n CompoundEntityRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\n\nexport type CachedEntityLoaderOptions = {\n catalog: CatalogClient;\n cache: CacheClient;\n};\n\nexport class CachedEntityLoader {\n private readonly catalog: CatalogClient;\n private readonly cache: CacheClient;\n private readonly readTimeout = 1000;\n\n constructor({ catalog, cache }: CachedEntityLoaderOptions) {\n this.catalog = catalog;\n this.cache = cache;\n }\n\n async load(\n entityRef: CompoundEntityRef,\n token: string | undefined,\n ): Promise<Entity | undefined> {\n const cacheKey = this.getCacheKey(entityRef, token);\n let result = await this.getFromCache(cacheKey);\n\n if (result) {\n return result;\n }\n\n result = await this.catalog.getEntityByRef(entityRef, { token });\n\n if (result) {\n this.cache.set(cacheKey, result, { ttl: 5000 });\n }\n\n return result;\n }\n\n private async getFromCache(key: string): Promise<Entity | undefined> {\n // Promise.race ensures we don't hang the client for long if the cache is\n // temporarily unreachable.\n return (await Promise.race([\n this.cache.get(key),\n new Promise(cancelAfter => setTimeout(cancelAfter, this.readTimeout)),\n ])) as Entity | undefined;\n }\n\n private getCacheKey(\n entityName: CompoundEntityRef,\n token: string | undefined,\n ): string {\n const key = ['catalog', stringifyEntityRef(entityName)];\n\n if (token) {\n key.push(token);\n }\n\n return key.join(':');\n }\n}\n","/*\n * Copyright 2022 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 { Entity } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\n\n/**\n * Parameters passed to the shouldBuild method on the DocsBuildStrategy interface\n *\n * @public\n */\nexport type ShouldBuildParameters = {\n entity: Entity;\n};\n\n/**\n * A strategy for when to build TechDocs locally, and when to skip building TechDocs (allowing for an external build)\n *\n * @public\n */\nexport interface DocsBuildStrategy {\n shouldBuild(params: ShouldBuildParameters): Promise<boolean>;\n}\n\nexport class DefaultDocsBuildStrategy {\n private readonly config: Config;\n\n private constructor(config: Config) {\n this.config = config;\n }\n\n static fromConfig(config: Config): DefaultDocsBuildStrategy {\n return new DefaultDocsBuildStrategy(config);\n }\n\n async shouldBuild(_: ShouldBuildParameters): Promise<boolean> {\n return this.config.getString('techdocs.builder') === 'local';\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 { 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 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';\nimport { CachedEntityLoader } from './CachedEntityLoader';\nimport {\n DefaultDocsBuildStrategy,\n DocsBuildStrategy,\n} from './DocsBuildStrategy';\n\n/**\n * Required dependencies for running TechDocs in the \"out-of-the-box\"\n * deployment configuration (prepare/generate/publish all in the Backend).\n *\n * @public\n */\nexport type 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 docsBuildStrategy?: DocsBuildStrategy;\n};\n\n/**\n * Required dependencies for running TechDocs in the \"recommended\" deployment\n * configuration (prepare/generate handled externally in CI/CD).\n *\n * @public\n */\nexport type RecommendedDeploymentOptions = {\n publisher: PublisherBase;\n logger: Logger;\n discovery: PluginEndpointDiscovery;\n config: Config;\n cache: PluginCacheManager;\n docsBuildStrategy?: DocsBuildStrategy;\n};\n\n/**\n * One of the two deployment configurations must be provided.\n *\n * @public\n */\nexport type 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 *\n * * @public\n */\nfunction isOutOfTheBoxOption(\n opt: RouterOptions,\n): opt is OutOfTheBoxDeploymentOptions {\n return (opt as OutOfTheBoxDeploymentOptions).preparers !== undefined;\n}\n\n/**\n * Creates a techdocs router.\n *\n * @public\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 docsBuildStrategy =\n options.docsBuildStrategy ?? DefaultDocsBuildStrategy.fromConfig(config);\n\n // Entities are cached to optimize the /static/docs request path, which can be called many times\n // when loading a single techdocs page.\n const entityLoader = new CachedEntityLoader({\n catalog: catalogClient,\n cache: options.cache.getClient(),\n });\n\n // Set up a cache client if configured.\n let cache: TechDocsCache | undefined;\n const defaultTtl = config.getOptionalNumber('techdocs.cache.ttl');\n if (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 const token = getBearerToken(req.headers.authorization);\n\n // Verify that the related entity exists and the current user has permission to view it.\n const entity = await entityLoader.load(entityName, token);\n\n if (!entity) {\n throw new NotFoundError(\n `Unable to get metadata for '${stringifyEntityRef(entityName)}'`,\n );\n }\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 { kind, namespace, name } = req.params;\n const entityName = { kind, namespace, name };\n const token = getBearerToken(req.headers.authorization);\n\n const entity = await entityLoader.load(entityName, token);\n\n if (!entity) {\n throw new NotFoundError(\n `Unable to get metadata for '${stringifyEntityRef(entityName)}'`,\n );\n }\n\n try {\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 entityLoader.load({ kind, namespace, name }, token);\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 // By default, techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to\n // 'local'. 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 // Altering the implementation of the injected docsBuildStrategy allows for more complex behaviours, based on\n // either config or the properties of the entity (e.g. annotations, labels, spec fields etc.).\n const shouldBuild = await docsBuildStrategy.shouldBuild({ entity });\n if (!shouldBuild) {\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. docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization.\",\n ),\n );\n });\n\n // Ensures that the related entity exists and the current user has permission to view it.\n if (config.getOptionalBoolean('permission.enabled')) {\n router.use(\n '/static/docs/:namespace/:kind/:name',\n async (req, _res, next) => {\n const { kind, namespace, name } = req.params;\n const entityName = { kind, namespace, name };\n const token = getBearerToken(req.headers.authorization);\n\n const entity = await entityLoader.load(entityName, token);\n\n if (!entity) {\n throw new NotFoundError(\n `Entity not found for ${stringifyEntityRef(entityName)}`,\n );\n }\n\n next();\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 * @deprecated use event-stream implementation of the sync endpoint\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 2022 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 {\n CatalogApi,\n CatalogClient,\n CATALOG_FILTER_EXISTS,\n} from '@backstage/catalog-client';\nimport {\n Entity,\n parseEntityRef,\n RELATION_OWNED_BY,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { catalogEntityReadPermission } from '@backstage/plugin-catalog-common';\nimport { DocumentCollatorFactory } from '@backstage/search-common';\nimport { TechDocsDocument } from '@backstage/techdocs-common';\nimport unescape from 'lodash/unescape';\nimport fetch from 'node-fetch';\nimport pLimit from 'p-limit';\nimport { Readable } from 'stream';\nimport { Logger } from 'winston';\n\ninterface MkSearchIndexDoc {\n title: string;\n text: string;\n location: string;\n}\n\n/**\n * Options to configure the TechDocs collator factory\n *\n * @public\n */\nexport type TechDocsCollatorFactoryOptions = {\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\n/**\n * A search collator factory responsible for gathering and transforming\n * TechDocs documents.\n *\n * @public\n */\nexport class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory {\n public readonly type: string = 'techdocs';\n public readonly visibilityPermission = catalogEntityReadPermission;\n\n private discovery: PluginEndpointDiscovery;\n private 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\n private constructor(options: TechDocsCollatorFactoryOptions) {\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: TechDocsCollatorFactoryOptions) {\n const legacyPathCasing =\n config.getOptionalBoolean(\n 'techdocs.legacyUseCaseSensitiveTripletPaths',\n ) || false;\n return new DefaultTechDocsCollatorFactory({ ...options, legacyPathCasing });\n }\n\n async getCollator(): Promise<Readable> {\n return Readable.from(this.execute());\n }\n\n private async *execute(): AsyncGenerator<TechDocsDocument, void, undefined> {\n const limit = pLimit(this.parallelismLimit);\n const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs');\n const { token } = await this.tokenManager.getToken();\n let entitiesRetrieved = 0;\n let moreEntitiesToGet = true;\n\n // Offset/limit pagination is used on the Catalog Client in order to\n // limit (and allow some control over) memory used by the search backend\n // at index-time. The batchSize is calculated as a factor of the given\n // parallelism limit to simplify configuration.\n const batchSize = this.parallelismLimit * 50;\n while (moreEntitiesToGet) {\n const entities = (\n await this.catalogClient.getEntities(\n {\n filter: {\n 'metadata.annotations.backstage.io/techdocs-ref':\n CATALOG_FILTER_EXISTS,\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 limit: batchSize,\n offset: entitiesRetrieved,\n },\n { token },\n )\n ).items;\n\n // Control looping through entity batches.\n moreEntitiesToGet = entities.length === batchSize;\n entitiesRetrieved += entities.length;\n\n const docPromises = entities\n .filter(it => it.metadata?.annotations?.['backstage.io/techdocs-ref'])\n .map((entity: Entity) =>\n limit(async (): Promise<TechDocsDocument[]> => {\n const entityInfo =\n DefaultTechDocsCollatorFactory.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 DefaultTechDocsCollatorFactory.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(\n this.locationTemplate || '/docs/:namespace/:kind/:name/:path',\n {\n ...entityInfo,\n path: doc.location,\n },\n ),\n path: doc.location,\n ...entityInfo,\n entityTitle: entity.metadata.title,\n componentType: entity.spec?.type?.toString() || 'other',\n lifecycle: (entity.spec?.lifecycle as string) || '',\n owner: getSimpleEntityOwnerString(entity),\n authorization: {\n resourceRef: stringifyEntityRef(entity),\n },\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 yield* (await Promise.all(docPromises)).flat();\n }\n }\n\n private 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\nfunction getSimpleEntityOwnerString(entity: Entity): string {\n if (entity.relations) {\n const owner = entity.relations.find(r => r.type === RELATION_OWNED_BY);\n if (owner) {\n const { name } = parseEntityRef(owner.targetRef);\n return name;\n }\n }\n return '';\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 {\n Entity,\n parseEntityRef,\n RELATION_OWNED_BY,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\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 { catalogEntityReadPermission } from '@backstage/plugin-catalog-common';\nimport {\n CatalogApi,\n CatalogClient,\n CATALOG_FILTER_EXISTS,\n} from '@backstage/catalog-client';\nimport { TechDocsDocument } from '@backstage/techdocs-common';\n\ninterface MkSearchIndexDoc {\n title: string;\n text: string;\n location: string;\n}\n\n/**\n * Options to configure the TechDocs collator\n *\n * @public\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\n/**\n * A search collator responsible for gathering and transforming TechDocs documents.\n *\n * @public\n * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and\n * use `DefaultTechDocsCollatorFactory` instead.\n */\nexport class DefaultTechDocsCollator {\n public readonly type: string = 'techdocs';\n public readonly visibilityPermission = catalogEntityReadPermission;\n\n private constructor(\n private readonly legacyPathCasing: boolean,\n private readonly options: TechDocsCollatorOptions,\n ) {}\n\n static fromConfig(config: Config, options: TechDocsCollatorOptions) {\n const legacyPathCasing =\n config.getOptionalBoolean(\n 'techdocs.legacyUseCaseSensitiveTripletPaths',\n ) || false;\n return new DefaultTechDocsCollator(legacyPathCasing, options);\n }\n\n async execute() {\n const {\n parallelismLimit,\n discovery,\n tokenManager,\n catalogClient,\n locationTemplate,\n logger,\n } = this.options;\n const limit = pLimit(parallelismLimit ?? 10);\n const techDocsBaseUrl = await discovery.getBaseUrl('techdocs');\n const { token } = await tokenManager.getToken();\n const entities = await (\n catalogClient ?? new CatalogClient({ discoveryApi: discovery })\n ).getEntities(\n {\n filter: {\n 'metadata.annotations.backstage.io/techdocs-ref':\n CATALOG_FILTER_EXISTS,\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.map((entity: Entity) =>\n limit(async (): Promise<TechDocsDocument[]> => {\n const entityInfo = DefaultTechDocsCollator.handleEntityInfoCasing(\n this.legacyPathCasing ?? false,\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(\n locationTemplate || '/docs/:namespace/:kind/:name/:path',\n {\n ...entityInfo,\n path: doc.location,\n },\n ),\n path: doc.location,\n ...entityInfo,\n entityTitle: entity.metadata.title,\n componentType: entity.spec?.type?.toString() || 'other',\n lifecycle: (entity.spec?.lifecycle as string) || '',\n owner: getSimpleEntityOwnerString(entity),\n authorization: {\n resourceRef: stringifyEntityRef(entity),\n },\n }));\n } catch (e) {\n 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\nfunction getSimpleEntityOwnerString(entity: Entity): string {\n if (entity.relations) {\n const owner = entity.relations.find(r => r.type === RELATION_OWNED_BY);\n if (owner) {\n const { name } = parseEntityRef(owner.targetRef);\n return name;\n }\n }\n return '';\n}\n"],"names":["stringifyEntityRef","DEFAULT_NAMESPACE","isError","os","fs","path","getLocationForEntity","UrlPreparer","winston","PassThrough","NotFoundError","fetch","router","CustomErrorBase","Router","catalogClient","CatalogClient","ScmIntegrations","NotModifiedError","catalogEntityReadPermission","Readable","pLimit","CATALOG_FILTER_EXISTS","unescape","getSimpleEntityOwnerString","RELATION_OWNED_BY","parseEntityRef"],"mappings":"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"}
@@ -0,0 +1,150 @@
1
+ /// <reference types="node" />
2
+ import { PluginEndpointDiscovery, PluginCacheManager, TokenManager } from '@backstage/backend-common';
3
+ import { Config } from '@backstage/config';
4
+ import { PreparerBuilder, GeneratorBuilder, PublisherBase, TechDocsDocument } from '@backstage/techdocs-common';
5
+ export * from '@backstage/techdocs-common';
6
+ export { TechDocsDocument } from '@backstage/techdocs-common';
7
+ import express from 'express';
8
+ import { Knex } from 'knex';
9
+ import { Logger } from 'winston';
10
+ import { Entity } from '@backstage/catalog-model';
11
+ import * as _backstage_plugin_permission_common from '@backstage/plugin-permission-common';
12
+ import { CatalogApi } from '@backstage/catalog-client';
13
+ import { DocumentCollatorFactory } from '@backstage/search-common';
14
+ import { Readable } from 'stream';
15
+
16
+ /**
17
+ * Parameters passed to the shouldBuild method on the DocsBuildStrategy interface
18
+ *
19
+ * @public
20
+ */
21
+ declare type ShouldBuildParameters = {
22
+ entity: Entity;
23
+ };
24
+ /**
25
+ * A strategy for when to build TechDocs locally, and when to skip building TechDocs (allowing for an external build)
26
+ *
27
+ * @public
28
+ */
29
+ interface DocsBuildStrategy {
30
+ shouldBuild(params: ShouldBuildParameters): Promise<boolean>;
31
+ }
32
+
33
+ /**
34
+ * Required dependencies for running TechDocs in the "out-of-the-box"
35
+ * deployment configuration (prepare/generate/publish all in the Backend).
36
+ *
37
+ * @public
38
+ */
39
+ declare type OutOfTheBoxDeploymentOptions = {
40
+ preparers: PreparerBuilder;
41
+ generators: GeneratorBuilder;
42
+ publisher: PublisherBase;
43
+ logger: Logger;
44
+ discovery: PluginEndpointDiscovery;
45
+ database?: Knex;
46
+ config: Config;
47
+ cache: PluginCacheManager;
48
+ docsBuildStrategy?: DocsBuildStrategy;
49
+ };
50
+ /**
51
+ * Required dependencies for running TechDocs in the "recommended" deployment
52
+ * configuration (prepare/generate handled externally in CI/CD).
53
+ *
54
+ * @public
55
+ */
56
+ declare type RecommendedDeploymentOptions = {
57
+ publisher: PublisherBase;
58
+ logger: Logger;
59
+ discovery: PluginEndpointDiscovery;
60
+ config: Config;
61
+ cache: PluginCacheManager;
62
+ docsBuildStrategy?: DocsBuildStrategy;
63
+ };
64
+ /**
65
+ * One of the two deployment configurations must be provided.
66
+ *
67
+ * @public
68
+ */
69
+ declare type RouterOptions = RecommendedDeploymentOptions | OutOfTheBoxDeploymentOptions;
70
+ /**
71
+ * Creates a techdocs router.
72
+ *
73
+ * @public
74
+ */
75
+ declare function createRouter(options: RouterOptions): Promise<express.Router>;
76
+
77
+ /**
78
+ * Options to configure the TechDocs collator factory
79
+ *
80
+ * @public
81
+ */
82
+ declare type TechDocsCollatorFactoryOptions = {
83
+ discovery: PluginEndpointDiscovery;
84
+ logger: Logger;
85
+ tokenManager: TokenManager;
86
+ locationTemplate?: string;
87
+ catalogClient?: CatalogApi;
88
+ parallelismLimit?: number;
89
+ legacyPathCasing?: boolean;
90
+ };
91
+ /**
92
+ * A search collator factory responsible for gathering and transforming
93
+ * TechDocs documents.
94
+ *
95
+ * @public
96
+ */
97
+ declare class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory {
98
+ readonly type: string;
99
+ readonly visibilityPermission: _backstage_plugin_permission_common.Permission;
100
+ private discovery;
101
+ private locationTemplate;
102
+ private readonly logger;
103
+ private readonly catalogClient;
104
+ private readonly tokenManager;
105
+ private readonly parallelismLimit;
106
+ private readonly legacyPathCasing;
107
+ private constructor();
108
+ static fromConfig(config: Config, options: TechDocsCollatorFactoryOptions): DefaultTechDocsCollatorFactory;
109
+ getCollator(): Promise<Readable>;
110
+ private execute;
111
+ private applyArgsToFormat;
112
+ private static constructDocsIndexUrl;
113
+ private static handleEntityInfoCasing;
114
+ }
115
+
116
+ /**
117
+ * Options to configure the TechDocs collator
118
+ *
119
+ * @public
120
+ */
121
+ declare type TechDocsCollatorOptions = {
122
+ discovery: PluginEndpointDiscovery;
123
+ logger: Logger;
124
+ tokenManager: TokenManager;
125
+ locationTemplate?: string;
126
+ catalogClient?: CatalogApi;
127
+ parallelismLimit?: number;
128
+ legacyPathCasing?: boolean;
129
+ };
130
+ /**
131
+ * A search collator responsible for gathering and transforming TechDocs documents.
132
+ *
133
+ * @public
134
+ * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and
135
+ * use `DefaultTechDocsCollatorFactory` instead.
136
+ */
137
+ declare class DefaultTechDocsCollator {
138
+ private readonly legacyPathCasing;
139
+ private readonly options;
140
+ readonly type: string;
141
+ readonly visibilityPermission: _backstage_plugin_permission_common.Permission;
142
+ private constructor();
143
+ static fromConfig(config: Config, options: TechDocsCollatorOptions): DefaultTechDocsCollator;
144
+ execute(): Promise<TechDocsDocument[]>;
145
+ protected applyArgsToFormat(format: string, args: Record<string, string>): string;
146
+ private static constructDocsIndexUrl;
147
+ private static handleEntityInfoCasing;
148
+ }
149
+
150
+ export { DefaultTechDocsCollator, DefaultTechDocsCollatorFactory, DocsBuildStrategy, OutOfTheBoxDeploymentOptions, RecommendedDeploymentOptions, RouterOptions, ShouldBuildParameters, TechDocsCollatorFactoryOptions, TechDocsCollatorOptions, createRouter };
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.13.4",
4
+ "version": "0.14.1",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "Apache-2.0",
@@ -34,15 +34,15 @@
34
34
  "clean": "backstage-cli package clean"
35
35
  },
36
36
  "dependencies": {
37
- "@backstage/backend-common": "^0.10.8",
38
- "@backstage/catalog-client": "^0.7.0",
39
- "@backstage/catalog-model": "^0.10.0",
40
- "@backstage/config": "^0.1.14",
41
- "@backstage/errors": "^0.2.1",
42
- "@backstage/integration": "^0.7.3",
43
- "@backstage/plugin-catalog-common": "^0.1.3",
44
- "@backstage/search-common": "^0.2.3",
45
- "@backstage/techdocs-common": "^0.11.8",
37
+ "@backstage/backend-common": "^0.12.0",
38
+ "@backstage/catalog-client": "^0.8.0",
39
+ "@backstage/catalog-model": "^0.12.0",
40
+ "@backstage/config": "^0.1.15",
41
+ "@backstage/errors": "^0.2.2",
42
+ "@backstage/integration": "^0.8.0",
43
+ "@backstage/plugin-catalog-common": "^0.2.0",
44
+ "@backstage/search-common": "^0.3.0",
45
+ "@backstage/techdocs-common": "^0.11.11",
46
46
  "@types/express": "^4.17.6",
47
47
  "dockerode": "^3.3.1",
48
48
  "express": "^4.17.1",
@@ -55,8 +55,9 @@
55
55
  "winston": "^3.2.1"
56
56
  },
57
57
  "devDependencies": {
58
- "@backstage/cli": "^0.14.0",
59
- "@backstage/test-utils": "^0.2.5",
58
+ "@backstage/cli": "^0.15.0",
59
+ "@backstage/plugin-search-backend-node": "0.5.0",
60
+ "@backstage/test-utils": "^0.3.0",
60
61
  "@types/dockerode": "^3.3.0",
61
62
  "msw": "^0.35.0",
62
63
  "supertest": "^6.1.3"
@@ -66,5 +67,5 @@
66
67
  "config.d.ts"
67
68
  ],
68
69
  "configSchema": "config.d.ts",
69
- "gitHead": "4805c3d13ce9bfc369e53c271b1b95e722b3b4dc"
70
+ "gitHead": "04bb0dd824b78f6b57dac62c3015e681f094045c"
70
71
  }