Merge branch 'master' into jhaals/dep-favoriteEntityTooltip
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-model': patch
|
||||
---
|
||||
|
||||
Move `@types/json-schema` to be a dev dependency
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/catalog-client': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Removed the old deprecated request/response types:
|
||||
|
||||
- `CatalogEntitiesRequest` - please use `GetEntitiesRequest` instead
|
||||
- `CatalogEntityAncestorsRequest` - please use `GetEntityAncestorsRequest` instead
|
||||
- `CatalogEntityAncestorsResponse` - please use `GetEntityAncestorsResponse` instead
|
||||
- `CatalogListResponse` - please use `GetEntitiesResponse` instead
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
---
|
||||
|
||||
Added a new interface that allows for customization of when to build techdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
---
|
||||
|
||||
add config option `groupExpand` to allow expanding a single relationship
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': patch
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
'@backstage/plugin-badges-backend': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-catalog-graph': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-code-coverage-backend': patch
|
||||
'@backstage/plugin-explore': patch
|
||||
'@backstage/plugin-fossa': patch
|
||||
'@backstage/plugin-jenkins-backend': patch
|
||||
'@backstage/plugin-rollbar': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
'@backstage/plugin-todo-backend': patch
|
||||
---
|
||||
|
||||
Use `getEntityByRef` instead of `getEntityByName` in the catalog client
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-catalog-graph': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-explore': patch
|
||||
'@backstage/plugin-fossa': patch
|
||||
'@backstage/plugin-org': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
- **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/integration': minor
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
- **BREAKING**: Removed the `FavouriteTemplate` export in favor of the `FavoriteEntity` from `@backstage/plugin-catalog-react`. Please migrate any usages to that component instead if you are creating your own `TemplateCard` page.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/catalog-model': patch
|
||||
---
|
||||
|
||||
**DEPRECATION**: Deprecated the `EntityName` type, and added the better-named `CompoundEntityRef` to replace it.
|
||||
|
||||
**DEPRECATION**: Deprecated the `getEntityName` function, and added the better-named `getCompoundEntityRef` to replace it.
|
||||
|
||||
Please switch over to using the new symbols, as the old ones may be removed in a future release.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Export FetchUrlReader to facilitate more flexible configuration of the backend.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Removed the deprecated `app.<key>` template variables from the `index.html` templating. These should be replaced by using `config.getString("app.<key>")` instead.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
- **BREAKING**: The `isOwnerOf` function has been marked as `@alpha` and is now only available via the `@backstage/plugin-catalog-react/alpha` import. The limitations of this function with regards to only supporting direct relations have also been documented.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-permission-node': patch
|
||||
---
|
||||
|
||||
Export some utility functions for parsing PermissionCriteria
|
||||
|
||||
`isAndCriteria`, `isOrCriteria`, `isNotCriteria` are now exported.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
---
|
||||
|
||||
add documentation for config options `userGroupMemberSearch` and `groupSearch`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-airbrake': patch
|
||||
'@backstage/plugin-airbrake-backend': patch
|
||||
---
|
||||
|
||||
The Airbrake plugin installation instructions have been updated to work better and conform to how the frontend and backend plugins are supposed to be integrated into a Backstage instance.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-client': patch
|
||||
---
|
||||
|
||||
**DEPRECATION**: Deprecated `getEntityByName` from `CatalogApi` and added `getEntityByRef` instead, which accepts both string and compound ref forms.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-node': minor
|
||||
'@backstage/search-common': minor
|
||||
---
|
||||
|
||||
**BREAKING**
|
||||
|
||||
The Backstage Search Platform's indexing process has been rewritten as a stream
|
||||
pipeline in order to improve efficiency and performance on large document sets.
|
||||
|
||||
The concepts of `Collator` and `Decorator` have been replaced with readable and
|
||||
transform object streams (respectively), as well as factory classes to
|
||||
instantiate them. Accordingly, the `SearchEngine.index()` method has also been
|
||||
replaced with a `getIndexer()` factory method that resolves to a writable
|
||||
object stream.
|
||||
|
||||
Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta)
|
||||
for further details.
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-pg': minor
|
||||
---
|
||||
|
||||
**BREAKING**
|
||||
|
||||
The `PgSearchEngine` implements the new stream-based indexing process expected
|
||||
by the latest `@backstage/search-backend-node`.
|
||||
|
||||
When updating to this version, you must also update to the latest version of
|
||||
`@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta)
|
||||
for further details.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
---
|
||||
|
||||
A `DefaultTechDocsCollatorFactory`, which works with the new stream-based
|
||||
search indexing subsystem, is now available. The `DefaultTechDocsCollator` will
|
||||
continue to be available for those unable to upgrade to the stream-based
|
||||
`@backstage/search-backend-node` (and related packages), however it is now
|
||||
marked as deprecated and will be removed in a future version.
|
||||
|
||||
To upgrade this plugin and the search indexing subsystem in one go, check
|
||||
[this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta)
|
||||
for necessary changes to your search backend plugin configuration.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
The Backstage Search Platform's indexing process has been rewritten as a stream
|
||||
pipeline in order to improve efficiency and performance on large document sets.
|
||||
|
||||
To take advantage of this, upgrade to the latest version of
|
||||
`@backstage/plugin-search-backend-node`, as well as any backend plugins whose
|
||||
collators you are using. Then, make the following changes to your
|
||||
`/packages/backend/src/plugins/search.ts` file:
|
||||
|
||||
```diff
|
||||
-import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend';
|
||||
-import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend';
|
||||
+import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend';
|
||||
+import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
|
||||
|
||||
// ...
|
||||
|
||||
const indexBuilder = new IndexBuilder({ logger, searchEngine });
|
||||
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
- collator: DefaultCatalogCollator.fromConfig(config, { discovery }),
|
||||
+ factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }),
|
||||
});
|
||||
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
- collator: DefaultTechDocsCollator.fromConfig(config, {
|
||||
+ factory: DefaultTechDocsCollatorFactory.fromConfig(config, {
|
||||
discovery,
|
||||
logger,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
If you've written custom collators, decorators, or search engines in your
|
||||
Backstage backend instance, you will need to re-implement them as readable,
|
||||
transform, and writable streams respectively (including factory classes for
|
||||
instantiating them). [A how-to guide for refactoring](https://backstage.io/docs/features/search/how-to-guides#rewriting-alpha-style-collators-for-beta)
|
||||
existing implementations is available.
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-elasticsearch': minor
|
||||
---
|
||||
|
||||
**BREAKING**
|
||||
|
||||
The `ElasticSearchSearchEngine` implements the new stream-based indexing
|
||||
process expected by the latest `@backstage/search-backend-node`.
|
||||
|
||||
When updating to this version, you must also update to the latest version of
|
||||
`@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta)
|
||||
for further details.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
A `DefaultCatalogCollatorFactory`, which works with the new stream-based
|
||||
search indexing subsystem, is now available. The `DefaultCatalogCollator` will
|
||||
continue to be available for those unable to upgrade to the stream-based
|
||||
`@backstage/search-backend-node` (and related packages), however it is now
|
||||
marked as deprecated and will be removed in a future version.
|
||||
|
||||
To upgrade this plugin and the search indexing subsystem in one go, check
|
||||
[this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta)
|
||||
for necessary changes to your search backend plugin configuration.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Removed the deprecated `GithubAuth.normalizeScopes` method.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `TokenFactory.issueToken` used by custom sign-in resolvers now ensures that the sub claim given is a full entity reference of the format `<kind>:<namespace>/<name>`. Any existing custom sign-in resolver functions that do not supply a full entity reference must be updated.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
---
|
||||
|
||||
add `userExpand` config option to allow expanding a single relationship
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Modify techdocs builder to automatically append techdocs-core plugin to mkdocs.yaml file if it is missing. Adds an optional configuration item if this plugin needs to be omitted.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
'@backstage/catalog-client': patch
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
'@backstage/plugin-catalog-graph': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-code-coverage': patch
|
||||
'@backstage/plugin-code-coverage-backend': patch
|
||||
'@backstage/plugin-fossa': patch
|
||||
'@backstage/plugin-jenkins': patch
|
||||
'@backstage/plugin-jenkins-backend': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
'@backstage/plugin-tech-insights': patch
|
||||
'@backstage/plugin-tech-insights-backend': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
'@backstage/plugin-todo-backend': patch
|
||||
---
|
||||
|
||||
Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
**DEPRECATION**: Deprecated the `RefreshIntervalFunction` and `createRandomRefreshInterval` in favour of the `ProcessingIntervalFunction` and `createRandomProcessingInterval` type and method respectively. Please migrate to use the new names.
|
||||
|
||||
**DEPRECATION**: Deprecated the `setRefreshInterval` and `setRefreshIntervalSeconds` methods on the `CatalogBuilder` for the new `setProcessingInterval` and `setProcessingIntervalSeconds` methods. Please migrate to use the new names.
|
||||
@@ -20,7 +20,6 @@ Autoscaling
|
||||
autoselect
|
||||
Avro
|
||||
aws
|
||||
backrub
|
||||
backported
|
||||
backporting
|
||||
Bigtable
|
||||
@@ -39,8 +38,6 @@ Changesets
|
||||
chanwit
|
||||
Chanwit
|
||||
ci
|
||||
cisphobia
|
||||
cissexist
|
||||
classname
|
||||
cli
|
||||
cloudbuild
|
||||
@@ -68,7 +65,6 @@ css
|
||||
Datadog
|
||||
dataflow
|
||||
dayjs
|
||||
deadnaming
|
||||
debounce
|
||||
Debounce
|
||||
declaratively
|
||||
@@ -171,7 +167,6 @@ Minikube
|
||||
Minio
|
||||
misconfiguration
|
||||
misconfigured
|
||||
misgendering
|
||||
mkdocs
|
||||
Mkdocs
|
||||
monorepo
|
||||
@@ -213,6 +208,7 @@ parallelization
|
||||
Patrik
|
||||
Peloton
|
||||
performant
|
||||
Performant
|
||||
plantuml
|
||||
Platformize
|
||||
Podman
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
# [Backstage](https://backstage.io)
|
||||
|
||||
_During March 7 to March 11 the maintainers will be taking part in Spotify's annual hack week. Development will continue as usual, but expect a slower pace for discussions and PR reviews. Why not take this opportunity to [build a plugin](https://backstage.io/docs/plugins/)?_
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://www.cncf.io/projects)
|
||||
[](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Search
|
||||
|
||||
Contributions/extensions to the Search plugin
|
||||
@@ -0,0 +1,95 @@
|
||||
ConfluenceCollator.ts reference
|
||||
|
||||
```ts
|
||||
import { DocumentCollator } from '@backstage/search-common';
|
||||
import fetch from 'cross-fetch';
|
||||
|
||||
export class ConfluenceCollator implements DocumentCollator {
|
||||
public readonly type: string = 'confluence';
|
||||
|
||||
async execute() {
|
||||
const ConfluenceUrlBase =
|
||||
'https://{CONFLUENCE-ORG-NAME}.atlassian.net/wiki/rest/api';
|
||||
|
||||
async function getConfluenceData(requestUrl: string) {
|
||||
var emptyJson = {};
|
||||
try {
|
||||
const res = await fetch(requestUrl, {
|
||||
method: 'get',
|
||||
headers: {
|
||||
Authorization: `Basic ${process.env.CONFLUENCE_TOKEN}`,
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
return await res.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return emptyJson;
|
||||
}
|
||||
|
||||
async function getSpaces(): Promise<string[]> {
|
||||
const data = await getConfluenceData(
|
||||
`${ConfluenceUrlBase}/space?&limit=1000&type=global&status=current`,
|
||||
);
|
||||
let spacesList = [];
|
||||
if (data['results']) {
|
||||
const results = data['results'];
|
||||
for (const result of results) {
|
||||
spacesList.push(result['key']);
|
||||
}
|
||||
}
|
||||
return spacesList;
|
||||
}
|
||||
|
||||
async function getDocumentsFromSpaces(spaces: string[]): Promise<string[]> {
|
||||
let documentsList = [];
|
||||
for (var space of spaces) {
|
||||
let next = true;
|
||||
let requestUrl = `${ConfluenceUrlBase}/content?limit=1000&status=current&spaceKey=${space}`;
|
||||
while (next) {
|
||||
const data = await getConfluenceData(requestUrl);
|
||||
if (data['results']) {
|
||||
const results = data['results'];
|
||||
for (const result of results) {
|
||||
documentsList.push(result['_links']['self']);
|
||||
}
|
||||
if (data['_links']['next']) {
|
||||
requestUrl = data['_links']['base'] + data['_links']['next'];
|
||||
} else {
|
||||
next = false;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return documentsList;
|
||||
}
|
||||
|
||||
async function getDocumentInfo(documents: string[]) {
|
||||
let documentInfo = [];
|
||||
for (var documentUrl of documents) {
|
||||
const data = await getConfluenceData(
|
||||
documentUrl + '?expand=body.storage',
|
||||
);
|
||||
if (data['status'] && data['status'] == 'current') {
|
||||
const documentMetaData = {
|
||||
title: data['title'],
|
||||
text: data['body']['storage']['value'],
|
||||
location: data['_links']['base'] + data['_links']['webui'],
|
||||
};
|
||||
documentInfo.push(documentMetaData);
|
||||
}
|
||||
}
|
||||
return documentInfo;
|
||||
}
|
||||
|
||||
const spacesList = await getSpaces();
|
||||
const documentsList = await getDocumentsFromSpaces(spacesList);
|
||||
const documentMetaDataList = await getDocumentInfo(documentsList);
|
||||
return documentMetaDataList;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
ConfluenceResultListItem.tsx reference
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { IndexableDocument } from '@backstage/search-common';
|
||||
import {
|
||||
Divider,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
result: IndexableDocument;
|
||||
};
|
||||
|
||||
export const ConfluenceResultListItem = ({ result }: Props) => {
|
||||
// Remove html tags from document text before displaying
|
||||
const chars = [];
|
||||
let isTag = false;
|
||||
for (const c of result.text.substring(0, 500)) {
|
||||
if (c === '<') {
|
||||
isTag = true;
|
||||
continue;
|
||||
}
|
||||
if (c === '>') {
|
||||
isTag = false;
|
||||
chars.push(' ');
|
||||
continue;
|
||||
}
|
||||
if (!isTag) {
|
||||
chars.push(c);
|
||||
}
|
||||
}
|
||||
const excerpt =
|
||||
chars.join('').substring(0, 80) + (result.text.length > 80 ? '...' : '');
|
||||
|
||||
return (
|
||||
<Link to={result.location}>
|
||||
<ListItem alignItems="center">
|
||||
<ListItemIcon>
|
||||
<img
|
||||
width="20"
|
||||
height="20"
|
||||
src="https://cdn.worldvectorlogo.com/logos/confluence-1.svg"
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={result.title}
|
||||
secondary={excerpt}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# Confluence
|
||||
|
||||
These files help you add Confluence as a source to the Backstage Search plugin.
|
||||
To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app.
|
||||
Then, add the following code to your packages/app/src/components/search/SearchPage.tsx:
|
||||
|
||||
```tsx
|
||||
import { ConfluenceResultListItem } from './ConfluenceResultListItem';
|
||||
```
|
||||
|
||||
```tsx
|
||||
case 'confluence':
|
||||
return (
|
||||
<ConfluenceResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
and the following to packages/backend/src/plugins/search.ts:
|
||||
|
||||
```ts
|
||||
import { ConfluenceCollator } from './search/ConfluenceCollator';
|
||||
```
|
||||
|
||||
```ts
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: new ConfluenceCollator(),
|
||||
});
|
||||
```
|
||||
@@ -54,13 +54,14 @@ An index is a collection of such documents of a given type.
|
||||
### Collators
|
||||
|
||||
You need to be able to search something! Collators are the way to define what
|
||||
can be searched. Specifically, they're classes which return documents conforming
|
||||
to a minimum set of fields (including a document title, location, and text), but
|
||||
which can contain any other fields as defined by the collator itself. One
|
||||
collator is responsible for defining and collecting documents of a type.
|
||||
can be searched. Specifically, they're readable object streams of documents that
|
||||
conform to a minimum set of fields (including a document title, location, and
|
||||
text), but which can contain any other fields as defined by the collator itself.
|
||||
One collator is responsible for defining and collecting documents of a type.
|
||||
|
||||
Some plugins, like the Catalog Backend, provide so-called "default" collators
|
||||
which you can use out-of-the-box to start searching across Backstage quickly.
|
||||
Some plugins, like the Catalog Backend, provide so-called "default" collator
|
||||
factories which you can use out-of-the-box to start searching across Backstage
|
||||
quickly.
|
||||
|
||||
### Decorators
|
||||
|
||||
@@ -68,9 +69,15 @@ Sometimes you want to add extra information to a set of documents in your search
|
||||
index that the collator may not be aware of. For example, the Software Catalog
|
||||
knows about software entities, but it may not know about their usage or quality.
|
||||
|
||||
Decorators are classes which can add extra fields to pre-collated documents.
|
||||
This extra metadata could then be used to bias search results or otherwise
|
||||
improve the search experience in your Backstage instance.
|
||||
Decorators are transform streams which sit between a collator (read stream) and
|
||||
an indexer (write stream) during the indexing process. It can be used to add
|
||||
extra fields to documents as they are being collated and indexed. This extra
|
||||
metadata could then be used to bias search results or otherwise improve the
|
||||
search experience in your Backstage instance.
|
||||
|
||||
In addition to adding extra metadata, decorators (like any transform stream) can
|
||||
also be used to remove metadata, filter out, or even add extra documents at
|
||||
index-time.
|
||||
|
||||
### The Scheduler
|
||||
|
||||
|
||||
@@ -48,10 +48,10 @@ const app = createApp({
|
||||
## How to index TechDocs documents
|
||||
|
||||
The TechDocs plugin has supported integrations to Search, meaning that it
|
||||
provides a default collator ready to be used.
|
||||
provides a default collator factory ready to be used.
|
||||
|
||||
The purpose of this guide is to walk you through how to register the
|
||||
[DefaultTechDocsCollator](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts)
|
||||
[DefaultTechDocsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts)
|
||||
in your App, so that you can get TechDocs documents indexed.
|
||||
|
||||
If you have been through the
|
||||
@@ -60,18 +60,19 @@ you should have the `packages/backend/src/plugins/search.ts` file available. If
|
||||
so, you can go ahead and follow this guide - if not, start by going through the
|
||||
getting started guide.
|
||||
|
||||
1. Import the DefaultTechDocsCollator from `@backstage/plugin-techdocs-backend`.
|
||||
1. Import the `DefaultTechDocsCollatorFactory` from
|
||||
`@backstage/plugin-techdocs-backend`.
|
||||
|
||||
```typescript
|
||||
import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend';
|
||||
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
|
||||
```
|
||||
|
||||
2. Register the DefaultTechDocsCollator with the IndexBuilder.
|
||||
2. Register the `DefaultTechDocsCollatorFactory` with the IndexBuilder.
|
||||
|
||||
```typescript
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: DefaultTechDocsCollator.fromConfig(config, {
|
||||
factory: DefaultTechDocsCollatorFactory.fromConfig(config, {
|
||||
discovery,
|
||||
logger,
|
||||
tokenManager,
|
||||
@@ -131,3 +132,264 @@ indexBuilder.addCollator({
|
||||
|
||||
As shown above, you can add a catalog entity filter to narrow down what catalog
|
||||
entities are indexed by the search engine.
|
||||
|
||||
## How to migrate from Search Alpha to Beta
|
||||
|
||||
For the purposes of this guide, Search Beta version is defined as:
|
||||
|
||||
- **Search Plugin**: At least `v0.x.y`
|
||||
- **Search Backend Plugin**: At least `v0.x.y`
|
||||
- **Search Backend Node**: At least `v0.x.y`
|
||||
|
||||
In the Beta version, the Search Platform's indexing process has been rewritten
|
||||
as a stream pipeline in order to improve efficiency and performance on large
|
||||
sets of documents.
|
||||
|
||||
If you've not yet extended the Search Platform with custom code, and have
|
||||
instead taken advantage of default collators, decorators, and search engines
|
||||
provided by existing plugins, the migration process is fairly straightforward:
|
||||
|
||||
1. Upgrade to at least version `0.x.y` of
|
||||
`@backstage/plugin-search-backend-node`, as well as any backend plugins whose
|
||||
collators you are using (e.g. at least version `0.x.y` of
|
||||
`@backstage/plugin-catalog-backend` and/or version `0.x.y` of
|
||||
`@backstage/plugin-techdocs-backend`).
|
||||
2. Then, make the following changes to your
|
||||
`/packages/backend/src/plugins/search.ts` file:
|
||||
|
||||
```diff
|
||||
-import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend';
|
||||
-import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend';
|
||||
+import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend';
|
||||
+import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
|
||||
// ...
|
||||
const indexBuilder = new IndexBuilder({ logger, searchEngine });
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
- collator: DefaultCatalogCollator.fromConfig(config, { discovery }),
|
||||
+ factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }),
|
||||
});
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
- collator: DefaultTechDocsCollator.fromConfig(config, {
|
||||
+ factory: DefaultTechDocsCollatorFactory.fromConfig(config, {
|
||||
discovery,
|
||||
logger,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
Any custom collators, decorators, or search engine implementations will require
|
||||
minor refactoring. Continue on for details.
|
||||
|
||||
### Rewriting alpha-style collators for beta
|
||||
|
||||
In alpha versions of the Backstage Search Platform, collators were classes that
|
||||
implemented an `execute` method which resolved an `IndexableDocument` array.
|
||||
|
||||
In beta versions, the logic encapsulated by the aforementioned `execute` method
|
||||
is contained within an [object-mode][obj-mode] `Readable` stream where each
|
||||
object pushed onto the stream is of type `IndexableDocument`. Instances of this
|
||||
stream are instantiated by a factory class conforming to the
|
||||
`DocumentCollatorFactory` interface.
|
||||
|
||||
The optimal conversion strategy will vary depending on the collator's logic, but
|
||||
the simplest conversion can follow a process like this:
|
||||
|
||||
1. Rename your collator class to something like `YourCollatorFactory` and update
|
||||
it to implement `DocumentCollatorFactory` instead of `DocumentCollator`.
|
||||
2. Update its `execute` method so that it resolves
|
||||
`AsyncGenerator<YourIndexableDocument>` instead of `YourIndexableDocument[]`.
|
||||
3. Implement `DocumentCollatorFactory`'s `getCollator` method which resolves to
|
||||
`Readable.from(this.execute())` (which is a utility for creating [readable
|
||||
streams][read-stream] from [async generators][async-gen]).
|
||||
|
||||
```ts
|
||||
import { DocumentCollatorFactory } from '@backstage/plugin-search-backend-node';
|
||||
import { Readable } from 'stream';
|
||||
export class YourCollatorFactory implements DocumentCollatorFactory {
|
||||
public readonly type: string = 'your-type';
|
||||
async *execute(): AsyncGenerator<YourIndexableDocument> {
|
||||
const widgets = await this.client.getWidgets();
|
||||
for (const widget of widgets) {
|
||||
yield {
|
||||
title: widget.name,
|
||||
location: widget.url,
|
||||
text: widget.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
getCollator() {
|
||||
return Readable.from(this.execute());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: it may be possible to simplify your collator dramatically! If your custom
|
||||
collator was previously using streams under the hood (for example, by reading
|
||||
newline delimited JSON from a local or remote file), you could just expose the
|
||||
stream directly via a simple factory class:
|
||||
|
||||
```ts
|
||||
import { DocumentCollatorFactory } from '@backstage/plugin-search-backend-node';
|
||||
import { createReadStream } from 'fs';
|
||||
import { parse } from '@jsonlines/core';
|
||||
export class YourCollatorFactory implements DocumentCollatorFactory {
|
||||
public readonly type: string = 'your-type';
|
||||
async getCollator() {
|
||||
const parseStream = parse();
|
||||
return createReadStream('./documents.ndjson').pipe(parseStream);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rewriting alpha-style decorators for beta
|
||||
|
||||
In alpha versions of the Backstage Search Platform, decorators were classes that
|
||||
implemented an `execute` method which took an `IndexableDocument` array as an
|
||||
argument, and resolved a modified array of the same type.
|
||||
|
||||
In beta versions, the logic encapsulated by the aforementioned `execute` method
|
||||
is contained within an object-mode `Transform` stream which reads objects of
|
||||
type `IndexableDocument`, and writes objects of a conforming type. Similar to
|
||||
collators, instances of this stream are instantiated by a factory class
|
||||
conforming to the `DocumentDecoratorFactory` interface.
|
||||
|
||||
Although you can choose to implement a `Transform` stream from scratch, the
|
||||
`@backstage/plugin-search-backend-node` package provides a `DecoratorBase` class
|
||||
in order to simplify the developer experience. With this base class, all that's
|
||||
needed is to transfer your old decorator class logic into the base class' three
|
||||
methods (`initialize`, `decorate`, and `finalize`), and implement the factory
|
||||
class that instantiates the stream:
|
||||
|
||||
```ts
|
||||
import { DecoratorBase } from '@backstage/plugin-search-backend-node';
|
||||
export class YourDecorator extends DecoratorBase {
|
||||
async initialize() {
|
||||
// Setup logic. Performed once before any documents are consumed.
|
||||
}
|
||||
async decorate(
|
||||
document: YourIndexableDocument,
|
||||
): Promise<YourIndexableDocument | YourIndexableDocument[] | undefined> {
|
||||
// Perform transformation logic here.
|
||||
return document;
|
||||
}
|
||||
async finalize() {
|
||||
// Teardown logic. Performed once after all documents have been consumed.
|
||||
}
|
||||
}
|
||||
export class YourDecoratorFactory implements DocumentDecoratorFactory {
|
||||
async getDecorator() {
|
||||
return new YourDecorator();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note the return type of the `decorate` method and how each can be used to
|
||||
different effect.
|
||||
|
||||
- By resolving a single `YourIndexableDocument` object, your decorator can be
|
||||
used to make simple transformations:
|
||||
|
||||
```ts
|
||||
class BooleanWidgetCoolnessDecorator extends DecoratorBase {
|
||||
async decorator(widget) {
|
||||
// Perform a simple, 1:1 transformation.
|
||||
widget.isCool = widget.isCool === 'true' ? true : false;
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- By resolving `undefined`, your decorator can filter out documents which
|
||||
shouldn't be in the index:
|
||||
|
||||
```ts
|
||||
class OnlyCoolWidgetsDecorator extends DecoratorBase {
|
||||
async decorator(widget) {
|
||||
// Perform a simple filter operation.
|
||||
return widget.isCool ? widget : undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- By resolving an array of `YourIndexableDocument` objects, you can generate
|
||||
multiple documents based on the content of one:
|
||||
|
||||
```ts
|
||||
class WidgetByVariantDecorator extends DecoratorBase {
|
||||
async decorator(widget) {
|
||||
// Generate one widget doc per widget variant.
|
||||
return widget.variants.map(variant => {
|
||||
// Each widget doc is the given widget plus a "variant" property
|
||||
// pulled from a widget.variants string array.
|
||||
return {
|
||||
...widget,
|
||||
variant,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In alpha versions, a decorator had access to every `IndexableDocument`
|
||||
simultaneously. This is no longer possible in beta versions (precisely to make
|
||||
the indexing process more efficient and performant). You will need to modify
|
||||
your decorator's logic so that it does not need access to every document at
|
||||
once.
|
||||
|
||||
### Rewriting alpha-style search engines for beta
|
||||
|
||||
Search Engines are responsible for both querying and indexing documents to an
|
||||
underlying search engine technology. While the search engine query interface
|
||||
didn't change between alpha and beta versions, the indexing half of the
|
||||
interface _did_ change.
|
||||
|
||||
In alpha versions of the Backstage Search Platform, a search engine implemented
|
||||
an `index` method which took a `type` and an `IndexableDocument` array and was
|
||||
responsible for writing these documents to the underlying search engine.
|
||||
|
||||
In beta versions, the logic encapsulated by the aforementioned `index` method is
|
||||
contained within an object-mode `Writable` stream which expects objects of type
|
||||
`IndexableDocument`. On the search engine class itself, the `index` method is
|
||||
replaced with a `getIndexer` factory method which still takes the `type`, but
|
||||
resolves an instance of the aforementioned `Writable` stream.
|
||||
|
||||
Although you can choose to implement a `Writable` stream from scratch, the
|
||||
`@backstage/plugin-search-backend-node` package provides a
|
||||
`BatchSearchEngineIndexer` class in order to simplify the developer experience.
|
||||
With this base class, which collects documents in batches of a configurable size
|
||||
on your behalf, all that's needed is to transfer your old `index` method logic
|
||||
into the base class' three methods (`initialize`, `index`, and `finalize`), and
|
||||
implement the factory method that instantiates the stream:
|
||||
|
||||
```ts
|
||||
import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';
|
||||
import { SearchEngine } from '@backstage/search-common';
|
||||
export class YourSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
constructor({ type }: { type: string }) {
|
||||
// Customize the number of documents passed to the index method per batch.
|
||||
super({ batchSize: 500 });
|
||||
// An imaginary search engine indexing client.
|
||||
this.index = new SomeSearchEngineIndex({ indexName: type });
|
||||
}
|
||||
async initialize() {
|
||||
// Setup logic. Performed once before any documents are consumed.
|
||||
}
|
||||
async index(documents: IndexableDocument[]) {
|
||||
await this.index.batchOf(documents);
|
||||
}
|
||||
async finalize() {
|
||||
// Teardown logic. Performed once after all documents have been consumed.
|
||||
}
|
||||
}
|
||||
export class YourSearchEngine implements SearchEngine {
|
||||
async getIndexer(type: string) {
|
||||
return new YourSearchEngineIndexer({ type });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[obj-mode]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_object_mode
|
||||
[read-stream]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_readable_streams
|
||||
[async-gen]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#iterating_over_async_generators
|
||||
|
||||
@@ -130,6 +130,8 @@ Options:
|
||||
if not found.
|
||||
--etag <ETAG> A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored
|
||||
in techdocs_metadata.json.
|
||||
--omitTechdocsCoreMkdocsPlugin An option to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files.
|
||||
Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file.
|
||||
-v --verbose Enable verbose output. (default: false)
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
@@ -46,6 +46,24 @@ between `techdocs-backend` and the storage)
|
||||
|
||||
[TechDocs Backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend)
|
||||
|
||||
## TechDocs Build Strategy
|
||||
|
||||
To accommodate more complex logic surrounding whether or not to build TechDocs, the TechDocs backend
|
||||
supports selecting a Build Strategy.
|
||||
The Build Strategy is responsible for deciding whether the documentation requested should be built locally
|
||||
by the TechDocs backend or not.
|
||||
Customization of the Build Strategy allows for more complex behaviour regarding whether the TechDocs backend
|
||||
is responsible for building TechDocs, whether an external process is responsible, or whether a combination
|
||||
of local builds and an external process is responsible, on an entity-by-entity basis.
|
||||
|
||||
The default Build Strategy results in the TechDocs backend building documentation locally if the
|
||||
`techdocs.builder` configuration option is set to `'local'`, and skipping any building otherwise.
|
||||
However any logic that satisfies the Build Strategy interface can be implemented, using the Backstage
|
||||
config as well as the entity being processed to make a decision.
|
||||
|
||||
For an example of how the Build Strategy can be used to implement a 'hybrid' build model, refer to
|
||||
the [How to implement a hybrid build strategy](./how-to-guides.md#how-to-implement-a-hybrid-build-strategy) guide.
|
||||
|
||||
## TechDocs Container
|
||||
|
||||
The TechDocs container is a Docker container available at
|
||||
|
||||
@@ -37,12 +37,22 @@ techdocs:
|
||||
|
||||
pullImage: true
|
||||
|
||||
mkdocs:
|
||||
# (Optional) techdocs.generator.omitTechdocsCoreMkdocsPlugin can be used to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files.
|
||||
# Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file.
|
||||
omitTechdocsCorePlugin: false
|
||||
|
||||
# techdocs.builder can be either 'local' or 'external.
|
||||
# If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to generate the docs, publish to storage
|
||||
# and show the generated docs afterwords. This is the "Basic" setup of the TechDocs Architecture.
|
||||
# If builder is set to 'external', techdocs-backend will only fetch the docs and will NOT try to generate and publish. In this case of 'external',
|
||||
# we assume that docs are being built by an external process (e.g. in the CI/CD pipeline of the repository). This is the "Recommended" setup of
|
||||
# the architecture. Read more here https://backstage.io/docs/features/techdocs/architecture
|
||||
# Using the default build strategy, if builder is set to 'local' and you open a TechDocs page,
|
||||
# techdocs-backend will try to generate the docs, publish to storage and show the generated docs afterwords.
|
||||
# This is the "Basic" setup of the TechDocs Architecture.
|
||||
# Using the default build strategy, if builder is set to 'external' (or anything other than 'local'), techdocs-backend
|
||||
# will only fetch the docs and will NOT try to generate and publish.
|
||||
# In this case, we assume that docs are being built by an external process (e.g. in the CI/CD pipeline of the repository).
|
||||
# This is the "Recommended" setup of the architecture.
|
||||
# Note that custom build strategies may alter this behaviour.
|
||||
# Read more about the "Basic" and "Recommended" setups here https://backstage.io/docs/features/techdocs/architecture
|
||||
# Read more about build strategies here: https://backstage.io/docs/features/techdocs/concepts#techdocs-build-strategy
|
||||
|
||||
builder: 'local'
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ plugins:
|
||||
- techdocs-core
|
||||
```
|
||||
|
||||
> Note - The plugins section above is optional. Backstage automatically adds the `techdocs-core` plugin to the
|
||||
> mkdocs file if it is missing. This functionality can be turned off with a [configuration option](./configuration.md) in Backstage.
|
||||
|
||||
Update your component's entity description by adding the following lines to its
|
||||
`catalog-info.yaml` in the root of its repository:
|
||||
|
||||
|
||||
@@ -538,3 +538,58 @@ Done! Now you have a support of the following diagrams along with mermaid:
|
||||
- `Vega`
|
||||
- `Vega-Lite`
|
||||
- `WaveDrom`
|
||||
|
||||
## How to implement a hybrid build strategy
|
||||
|
||||
One limitation of the [Recommended deployment](./architecture.md#recommended-deployment) is that
|
||||
the experience for users requires modifying their CI/CD process to publish
|
||||
their TechDocs. For some users, this may be unnecessary, and provides a barrier
|
||||
to entry for onboarding users to Backstage. However, a purely local TechDocs
|
||||
build restricts TechDocs creators to using the tooling provided in Backstage,
|
||||
as well as the plugins and features provided in the Backstage-included `mkdocs`
|
||||
installation.
|
||||
|
||||
To accommodate both of these use-cases, users can implement a custom [Build Strategy](./concepts.md#techdocs-build-strategy)
|
||||
with logic to encode which TechDocs should be built locally, and which will be
|
||||
built externally.
|
||||
|
||||
To achieve this hybrid build model:
|
||||
|
||||
1. In your Backstage instance's `app-config.yaml`, set `techdocs.builder` to
|
||||
`'local'`. This ensures that Backstage will build docs for users who want the
|
||||
'out-of-the-box' experience.
|
||||
2. Configure external storage of TechDocs as normal for a production deployment.
|
||||
This allows Backstage to publish documentation to your storage, as well as
|
||||
allowing other users to publish documentation from their CI/CD pipelines.
|
||||
3. Create a custom build strategy, that implements the `DocsBuildStrategy` interface,
|
||||
and which implements your custom logic for determining whether to build docs for
|
||||
a given entity.
|
||||
For example, to only build docs when an entity has the `company.com/techdocs-builder`
|
||||
annotation set to `'local'`:
|
||||
|
||||
```typescript
|
||||
export class AnnotationBasedBuildStrategy {
|
||||
private readonly config: Config;
|
||||
|
||||
constructor(config: Config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async shouldBuild(_: Entity): Promise<boolean> {
|
||||
return (
|
||||
this.entity.metadata?.annotations?.['company.com/techdocs-builder'] ===
|
||||
'local'
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Pass an instance of this Build Strategy as the `docsBuildStrategy` parameter of the
|
||||
TechDocs backend `createRouter` method.
|
||||
|
||||
Users should now be able to choose to have their documentation built and published by
|
||||
the TechDocs backend by adding the `company.com/techdocs-builder` annotation to their
|
||||
entity. If the value of this annotation is `'local'`, the TechDocs backend will build
|
||||
and publish the documentation for them. If the value of the `company.com/techdocs-builder`
|
||||
annotation is anything other than `'local'`, the user is responsible for publishing
|
||||
documentation to the appropriate location in the TechDocs external storage.
|
||||
|
||||
@@ -552,3 +552,36 @@ The following is an excerpt of a typical setup of an isomorphic library package:
|
||||
},
|
||||
"files": ["dist"],
|
||||
```
|
||||
|
||||
## Experimental Type Build
|
||||
|
||||
The Backstage CLI has an experimental feature where multiple different type definition files can be generated for different release stages. The release stages are marked in the [TSDoc](https://tsdoc.org/) for each individual export, using either `@public`, `@alpha`, or `@beta`. Rather than just building a single `index.d.ts` file, the build process will instead output `index.d.ts`, `index.beta.d.ts`, and `index.alpha.d.ts`. Each of these files will have exports from more unstable release stages stripped, meaning that `index.d.ts` will omit all exports marked with `@alpha` or `@beta`, while `index.beta.d.ts` will omit all exports marked with `@alpha`.
|
||||
|
||||
This feature is aimed at projects that publish to package registries and wish to maintain different levels of API stability within each package. There is no need to use this within a single monorepo, as it has no effect due to only applying to built and published packages.
|
||||
|
||||
In order for the experimental type build to work, `@microsoft/api-extractor` must be installed in your project, as it is an optional peer dependency of the Backstage CLI. There are then three steps that need to be taken for each package where you want to enable this feature:
|
||||
|
||||
- Add the `--experimental-type-build` flag to the `"build"` script of the package.
|
||||
- Add either one or both of `"alphaTypes"` and `"betaTypes"` to the `"publishConfig"` of the package:
|
||||
```json
|
||||
"publishConfig": {
|
||||
...
|
||||
"types": "dist/index.d.ts",
|
||||
"alphaTypes": "dist/index.alpha.d.ts",
|
||||
"betaTypes": "dist/index.beta.d.ts"
|
||||
},
|
||||
```
|
||||
- Add either one or both of `"alpha"` and `"beta"` to the `"files"` of the package:
|
||||
```json
|
||||
"files": [
|
||||
"dist",
|
||||
"alpha",
|
||||
"beta"
|
||||
]
|
||||
```
|
||||
|
||||
Once this setup is complete, users of the published packages will only be able to access the stable API via the main package entry point, for example `@acme/my-plugin`. Exports marked with `@alpha` or `@beta` will only be available via the `/alpha` entry point, for example `@acme/my-plugin/alpha`, and exports marked with `@beta` will only be available via `/beta`. This does not apply within the monorepo that contains the package. There all exports still have to be imported via the main entry point.
|
||||
|
||||
Note that these different entry points are only separated during type checking. At runtime they all share the same code which contains the exports from all releases stages.
|
||||
|
||||
An example of this setup can be seen in the [`@backstage/catalog-model`](https://github.com/backstage/backstage/blob/da0675bf9f28ed1460f03635a22d3c26abd14707/packages/catalog-model/package.json#L14) package, which has enabled `alpha` type exports. With this setup, exports marked as `@alpha` are only available for import via `@backstage/catalog-model/alpha`. The `@backstage/catalog-model` package currently does not have any exports marked as `@beta`, or a `/beta` entry point.
|
||||
|
||||
@@ -67,3 +67,12 @@ process is used to release an emergency fix as version `6.5.1` in the patch rele
|
||||
- [ ] The fix, which you can likely cherry-pick from your patch branch: `git cherry-pick origin/patch/v1.18.0^`
|
||||
- [ ] An updated `CHANGELOG.md` of all patched packages from the tip of the patch branch, `git checkout origin/patch/v1.18.0 -- {packages,plugins}/*/CHANGELOG.md`.
|
||||
- [ ] A changeset with the message "Applied the fix from version `6.5.1` of this package, which is part of the `v1.18.1` release of Backstage."
|
||||
- [ ] An entry in `.changeset/patched.json` that sets the current release version to `6.5.1`:
|
||||
|
||||
```json
|
||||
{
|
||||
"currentReleaseVersion": {
|
||||
"@backstage/plugin-foo": "6.5.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -234,6 +234,21 @@ export type ErrorHandlerOptions = {
|
||||
logClientErrors?: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class FetchUrlReader implements UrlReader {
|
||||
static factory: ReaderFactory;
|
||||
// (undocumented)
|
||||
read(url: string): Promise<Buffer>;
|
||||
// (undocumented)
|
||||
readTree(): Promise<ReadTreeResponse>;
|
||||
// (undocumented)
|
||||
readUrl(url: string, options?: ReadUrlOptions): Promise<ReadUrlResponse>;
|
||||
// (undocumented)
|
||||
search(): Promise<SearchResponse>;
|
||||
// (undocumented)
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type FromReadableArrayOptions = Array<{
|
||||
data: Readable;
|
||||
|
||||
@@ -270,22 +270,6 @@ describe('BitbucketUrlReader', () => {
|
||||
|
||||
expect(response.etag).toBe('12ab34cd56ef');
|
||||
});
|
||||
|
||||
it('should throw error when apiBaseUrl is missing', () => {
|
||||
expect(() => {
|
||||
/* eslint-disable no-new */
|
||||
new BitbucketUrlReader(
|
||||
new BitbucketIntegration(
|
||||
readBitbucketIntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'bitbucket.mycompany.net',
|
||||
}),
|
||||
),
|
||||
),
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
}).toThrowError('must configure an explicit apiBaseUrl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('search hosted', () => {
|
||||
|
||||
@@ -62,14 +62,9 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
private readonly integration: BitbucketIntegration,
|
||||
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
|
||||
) {
|
||||
const { host, apiBaseUrl, token, username, appPassword } =
|
||||
integration.config;
|
||||
const { host, token, username, appPassword } = integration.config;
|
||||
|
||||
if (!apiBaseUrl) {
|
||||
throw new Error(
|
||||
`Bitbucket integration for '${host}' must configure an explicit apiBaseUrl`,
|
||||
);
|
||||
} else if (!token && username && !appPassword) {
|
||||
if (!token && username && !appPassword) {
|
||||
throw new Error(
|
||||
`Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`,
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ export { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
export { GithubUrlReader } from './GithubUrlReader';
|
||||
export { GitlabUrlReader } from './GitlabUrlReader';
|
||||
export { AwsS3UrlReader } from './AwsS3UrlReader';
|
||||
export { FetchUrlReader } from './FetchUrlReader';
|
||||
export type {
|
||||
FromReadableArrayOptions,
|
||||
ReaderFactory,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useHotCleanup,
|
||||
} from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend';
|
||||
import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend';
|
||||
import { createRouter } from '@backstage/plugin-search-backend';
|
||||
import { ElasticSearchSearchEngine } from '@backstage/plugin-search-backend-module-elasticsearch';
|
||||
import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg';
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
LunrSearchEngine,
|
||||
SearchEngine,
|
||||
} from '@backstage/plugin-search-backend-node';
|
||||
import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend';
|
||||
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
|
||||
import { Logger } from 'winston';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
@@ -70,7 +70,7 @@ export default async function createPlugin({
|
||||
// particular collator gathers entities from the software catalog.
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: DefaultCatalogCollator.fromConfig(config, {
|
||||
factory: DefaultCatalogCollatorFactory.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
}),
|
||||
@@ -78,7 +78,7 @@ export default async function createPlugin({
|
||||
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: DefaultTechDocsCollator.fromConfig(config, {
|
||||
factory: DefaultTechDocsCollatorFactory.fromConfig(config, {
|
||||
discovery,
|
||||
logger,
|
||||
tokenManager,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
|
||||
// @public
|
||||
export type AddLocationRequest = {
|
||||
@@ -38,8 +38,13 @@ export interface CatalogApi {
|
||||
request: GetEntityAncestorsRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<GetEntityAncestorsResponse>;
|
||||
// @deprecated
|
||||
getEntityByName(
|
||||
name: EntityName,
|
||||
name: CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined>;
|
||||
getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined>;
|
||||
getEntityFacets(
|
||||
@@ -91,7 +96,11 @@ export class CatalogClient implements CatalogApi {
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<GetEntityAncestorsResponse>;
|
||||
getEntityByName(
|
||||
compoundName: EntityName,
|
||||
compoundName: CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined>;
|
||||
getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined>;
|
||||
getEntityFacets(
|
||||
@@ -130,18 +139,6 @@ export class CatalogClient implements CatalogApi {
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type CatalogEntitiesRequest = GetEntitiesRequest;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type CatalogEntityAncestorsRequest = GetEntityAncestorsRequest;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type CatalogEntityAncestorsResponse = GetEntityAncestorsResponse;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type CatalogListResponse<_Entity> = GetEntitiesResponse;
|
||||
|
||||
// @public
|
||||
export interface CatalogRequestOptions {
|
||||
// (undocumented)
|
||||
|
||||
@@ -195,6 +195,60 @@ describe('CatalogClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntityByRef', () => {
|
||||
const existingEntity: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'CustomKind',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'exists',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/entities/by-name/customkind/default/exists`,
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.json(existingEntity));
|
||||
},
|
||||
),
|
||||
rest.get(
|
||||
`${mockBaseUrl}/entities/by-name/customkind/default/missing`,
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(404));
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('finds by string and compound', async () => {
|
||||
await expect(
|
||||
client.getEntityByRef('customkind:default/exists'),
|
||||
).resolves.toEqual(existingEntity);
|
||||
await expect(
|
||||
client.getEntityByRef({
|
||||
kind: 'CustomKind',
|
||||
namespace: 'default',
|
||||
name: 'exists',
|
||||
}),
|
||||
).resolves.toEqual(existingEntity);
|
||||
});
|
||||
|
||||
it('returns undefined for 404s', async () => {
|
||||
await expect(
|
||||
client.getEntityByRef('customkind:default/missing'),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(
|
||||
client.getEntityByRef({
|
||||
kind: 'CustomKind',
|
||||
namespace: 'default',
|
||||
name: 'missing',
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocationById', () => {
|
||||
const defaultResponse = {
|
||||
data: {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
ANNOTATION_LOCATION,
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
Entity,
|
||||
EntityName,
|
||||
CompoundEntityRef,
|
||||
parseEntityRef,
|
||||
stringifyEntityRef,
|
||||
stringifyLocationRef,
|
||||
@@ -170,11 +170,32 @@ export class CatalogClient implements CatalogApi {
|
||||
return { items: entities.sort(refCompare) };
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc CatalogApi.getEntityByRef}
|
||||
*/
|
||||
async getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined> {
|
||||
const { kind, namespace, name } = parseEntityRef(entityRef);
|
||||
return this.requestOptional(
|
||||
'GET',
|
||||
`/entities/by-name/${encodeURIComponent(kind)}/${encodeURIComponent(
|
||||
namespace,
|
||||
)}/${encodeURIComponent(name)}`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE(freben): When we deprecate getEntityByName from the interface, we may
|
||||
// still want to leave this implementation in place for quite some time
|
||||
// longer, to minimize the risk for breakages. Suggested date for removal:
|
||||
// August 2022
|
||||
/**
|
||||
* {@inheritdoc CatalogApi.getEntityByName}
|
||||
*/
|
||||
async getEntityByName(
|
||||
compoundName: EntityName,
|
||||
compoundName: CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined> {
|
||||
const { kind, namespace = 'default', name } = compoundName;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
* This symbol can be used in place of a value when passed to filters in e.g.
|
||||
@@ -306,11 +306,25 @@ export interface CatalogApi {
|
||||
* Gets a single entity from the catalog by its ref (kind, namespace, name)
|
||||
* triplet.
|
||||
*
|
||||
* @param entityRef - A complete entity ref, either on string or compound form
|
||||
* @param options - Additional options
|
||||
* @returns The matching entity, or undefined if there was no entity with that ref
|
||||
*/
|
||||
getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined>;
|
||||
|
||||
/**
|
||||
* Gets a single entity from the catalog by its ref (kind, namespace, name)
|
||||
* triplet.
|
||||
*
|
||||
* @deprecated Use getEntityRef instead
|
||||
* @param name - A complete entity ref
|
||||
* @param options - Additional options
|
||||
*/
|
||||
getEntityByName(
|
||||
name: EntityName,
|
||||
name: CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined>;
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
GetEntitiesRequest,
|
||||
GetEntitiesResponse,
|
||||
GetEntityAncestorsRequest,
|
||||
GetEntityAncestorsResponse,
|
||||
} from './api';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use GetEntitiesRequest instead
|
||||
*/
|
||||
export type CatalogEntitiesRequest = GetEntitiesRequest;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use GetEntitiesResponse instead
|
||||
*/
|
||||
export type CatalogListResponse<_Entity> = GetEntitiesResponse;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use GetEntityAncestorsRequest instead
|
||||
*/
|
||||
export type CatalogEntityAncestorsRequest = GetEntityAncestorsRequest;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use GetEntityAncestorsResponse instead
|
||||
*/
|
||||
export type CatalogEntityAncestorsResponse = GetEntityAncestorsResponse;
|
||||
@@ -28,5 +28,4 @@ export type {
|
||||
GetEntityFacetsRequest,
|
||||
GetEntityFacetsResponse,
|
||||
} from './api';
|
||||
export * from './deprecated';
|
||||
export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status';
|
||||
|
||||
@@ -88,6 +88,13 @@ export { ComponentEntityV1alpha1 };
|
||||
// @public
|
||||
export const componentEntityV1alpha1Validator: KindValidator;
|
||||
|
||||
// @public
|
||||
export type CompoundEntityRef = {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const DEFAULT_NAMESPACE = 'default';
|
||||
|
||||
@@ -166,12 +173,8 @@ export type EntityMeta = JsonObject & {
|
||||
links?: EntityLink[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export type EntityName = {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
};
|
||||
// @public @deprecated
|
||||
export type EntityName = CompoundEntityRef;
|
||||
|
||||
// @public
|
||||
export const EntityPolicies: {
|
||||
@@ -196,7 +199,7 @@ export type EntityRef =
|
||||
// @public
|
||||
export type EntityRelation = {
|
||||
type: string;
|
||||
target: EntityName;
|
||||
target: CompoundEntityRef;
|
||||
targetRef: string;
|
||||
};
|
||||
|
||||
@@ -229,7 +232,10 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
|
||||
}
|
||||
|
||||
// @public
|
||||
export function getEntityName(entity: Entity): EntityName;
|
||||
export function getCompoundEntityRef(entity: Entity): CompoundEntityRef;
|
||||
|
||||
// @public @deprecated
|
||||
export const getEntityName: typeof getCompoundEntityRef;
|
||||
|
||||
// @public
|
||||
export function getEntitySourceLocation(entity: Entity): {
|
||||
@@ -337,7 +343,7 @@ export function parseEntityName(
|
||||
defaultKind?: string;
|
||||
defaultNamespace?: string;
|
||||
},
|
||||
): EntityName;
|
||||
): CompoundEntityRef;
|
||||
|
||||
// @public
|
||||
export function parseEntityRef(
|
||||
@@ -352,7 +358,7 @@ export function parseEntityRef(
|
||||
defaultKind?: string;
|
||||
defaultNamespace?: string;
|
||||
},
|
||||
): EntityName;
|
||||
): CompoundEntityRef;
|
||||
|
||||
// @public
|
||||
export function parseLocationRef(ref: string): {
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"@backstage/config": "^0.1.15",
|
||||
"@backstage/errors": "^0.2.2",
|
||||
"@backstage/types": "^0.1.3",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"ajv": "^7.0.3",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -46,6 +45,7 @@
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.14.1",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"yaml": "^1.9.2"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { EntityName } from '../types';
|
||||
import { CompoundEntityRef } from '../types';
|
||||
import { EntityStatus } from './EntityStatus';
|
||||
|
||||
/**
|
||||
@@ -201,7 +201,7 @@ export type EntityRelation = {
|
||||
*
|
||||
* @deprecated use targetRef instead
|
||||
*/
|
||||
target: EntityName;
|
||||
target: CompoundEntityRef;
|
||||
|
||||
/**
|
||||
* The entity ref of the target of this relation.
|
||||
|
||||
@@ -34,6 +34,7 @@ export type {
|
||||
} from './EntityStatus';
|
||||
export * from './policies';
|
||||
export {
|
||||
getCompoundEntityRef,
|
||||
getEntityName,
|
||||
parseEntityName,
|
||||
parseEntityRef,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { DEFAULT_NAMESPACE } from './constants';
|
||||
import { EntityName } from '../types';
|
||||
import { CompoundEntityRef } from '../types';
|
||||
import { Entity } from './Entity';
|
||||
|
||||
function parseRefString(ref: string): {
|
||||
@@ -38,14 +38,25 @@ function parseRefString(ref: string): {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the kind, namespace and name that form the name triplet of the
|
||||
* given entity.
|
||||
* Extracts the kind, namespace and name that form the compound entity ref
|
||||
* triplet of the given entity.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use getCompoundEntityRef instead
|
||||
* @param entity - An entity
|
||||
* @returns The compound entity ref
|
||||
*/
|
||||
export const getEntityName = getCompoundEntityRef;
|
||||
|
||||
/**
|
||||
* Extracts the kind, namespace and name that form the compound entity ref
|
||||
* triplet of the given entity.
|
||||
*
|
||||
* @public
|
||||
* @param entity - An entity
|
||||
* @returns The complete entity name
|
||||
* @returns The compound entity ref
|
||||
*/
|
||||
export function getEntityName(entity: Entity): EntityName {
|
||||
export function getCompoundEntityRef(entity: Entity): CompoundEntityRef {
|
||||
return {
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace || DEFAULT_NAMESPACE,
|
||||
@@ -77,7 +88,7 @@ export function parseEntityName(
|
||||
/** The default namespace, if none is given in the reference */
|
||||
defaultNamespace?: string;
|
||||
} = {},
|
||||
): EntityName {
|
||||
): CompoundEntityRef {
|
||||
const { kind, namespace, name } = parseEntityRef(ref, {
|
||||
defaultNamespace: DEFAULT_NAMESPACE,
|
||||
...context,
|
||||
@@ -114,7 +125,7 @@ export function parseEntityRef(
|
||||
/** The default namespace, if none is given in the reference */
|
||||
defaultNamespace?: string;
|
||||
},
|
||||
): EntityName {
|
||||
): CompoundEntityRef {
|
||||
if (!ref) {
|
||||
throw new Error(`Entity reference must not be empty`);
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@ export * from './entity';
|
||||
export { EntityPolicies } from './EntityPolicies';
|
||||
export * from './kinds';
|
||||
export * from './location';
|
||||
export type { EntityName, EntityRef } from './types';
|
||||
export type { EntityName, EntityRef, CompoundEntityRef } from './types';
|
||||
export * from './validation';
|
||||
|
||||
@@ -15,16 +15,25 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* A complete entity name, with the full kind-namespace-name triplet.
|
||||
* All parts of a complete entity ref, forming a full kind-namespace-name
|
||||
* triplet.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityName = {
|
||||
export type CompoundEntityRef = {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A complete entity name, with the full kind-namespace-name triplet.
|
||||
*
|
||||
* @deprecated Use CompoundEntityRef instead
|
||||
* @public
|
||||
*/
|
||||
export type EntityName = CompoundEntityRef;
|
||||
|
||||
/**
|
||||
* A reference by name to an entity, either as a compact string representation,
|
||||
* or as a compound reference structure.
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
@@ -118,43 +117,11 @@ export async function createConfig(
|
||||
}),
|
||||
);
|
||||
|
||||
const appParamDeprecationMsg = chalk.red(
|
||||
'DEPRECATION WARNING: using `app.<key>` in the index.html template is deprecated, use `config.getString("app.<key>")` instead.',
|
||||
);
|
||||
plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
template: paths.targetHtml,
|
||||
templateParameters: {
|
||||
publicPath: validBaseUrl.pathname.replace(/\/$/, ''),
|
||||
app: {
|
||||
get title() {
|
||||
console.warn(appParamDeprecationMsg);
|
||||
return frontendConfig.getString('app.title');
|
||||
},
|
||||
get baseUrl() {
|
||||
console.warn(appParamDeprecationMsg);
|
||||
return validBaseUrl.href;
|
||||
},
|
||||
get googleAnalyticsTrackingId() {
|
||||
console.warn(appParamDeprecationMsg);
|
||||
return frontendConfig.getOptionalString(
|
||||
'app.googleAnalyticsTrackingId',
|
||||
);
|
||||
},
|
||||
get datadogRum() {
|
||||
console.warn(appParamDeprecationMsg);
|
||||
return {
|
||||
env: frontendConfig.getOptionalString('app.datadogRum.env'),
|
||||
clientToken: frontendConfig.getOptionalString(
|
||||
'app.datadogRum.clientToken',
|
||||
),
|
||||
applicationId: frontendConfig.getOptionalString(
|
||||
'app.datadogRum.applicationId',
|
||||
),
|
||||
site: frontendConfig.getOptionalString('app.datadogRum.site'),
|
||||
};
|
||||
},
|
||||
},
|
||||
config: frontendConfig,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -182,7 +182,7 @@ function compileTsSchemas(paths: string[]) {
|
||||
program,
|
||||
// All schemas should export a `Config` symbol
|
||||
'Config',
|
||||
// This enables usage of @visibility and @deprecated in doc comments
|
||||
// This enables the use of these tags in TSDoc comments
|
||||
{
|
||||
required: true,
|
||||
validationKeywords: ['visibility', 'deprecated'],
|
||||
|
||||
@@ -376,8 +376,6 @@ export type FlatRoutesProps = {
|
||||
export class GithubAuth {
|
||||
// (undocumented)
|
||||
static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T;
|
||||
// @deprecated (undocumented)
|
||||
static normalizeScope(scope?: string): Set<string>;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -47,19 +47,4 @@ export default class GithubAuth {
|
||||
defaultScopes,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is deprecated and will be removed in a future release.
|
||||
*/
|
||||
static normalizeScope(scope?: string): Set<string> {
|
||||
if (!scope) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const scopeList = Array.isArray(scope)
|
||||
? scope
|
||||
: scope.split(/[\s|,]/).filter(Boolean);
|
||||
|
||||
return new Set(scopeList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
LunrSearchEngine,
|
||||
} from '@backstage/plugin-search-backend-node';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend';
|
||||
import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend';
|
||||
import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend';
|
||||
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
@@ -23,7 +23,7 @@ export default async function createPlugin({
|
||||
// collator gathers entities from the software catalog.
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: DefaultCatalogCollator.fromConfig(config, {
|
||||
factory: DefaultCatalogCollatorFactory.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
}),
|
||||
@@ -32,7 +32,7 @@ export default async function createPlugin({
|
||||
// collator gathers entities from techdocs.
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: DefaultTechDocsCollator.fromConfig(config, {
|
||||
factory: DefaultTechDocsCollatorFactory.fromConfig(config, {
|
||||
discovery,
|
||||
logger,
|
||||
tokenManager,
|
||||
|
||||
@@ -88,7 +88,7 @@ export class BitbucketIntegration implements ScmIntegration {
|
||||
// @public
|
||||
export type BitbucketIntegrationConfig = {
|
||||
host: string;
|
||||
apiBaseUrl?: string;
|
||||
apiBaseUrl: string;
|
||||
token?: string;
|
||||
username?: string;
|
||||
appPassword?: string;
|
||||
|
||||
@@ -36,12 +36,10 @@ export type BitbucketIntegrationConfig = {
|
||||
* The base URL of the API of this provider, e.g. "https://api.bitbucket.org/2.0",
|
||||
* with no trailing slash.
|
||||
*
|
||||
* May be omitted specifically for Bitbucket Cloud; then it will be deduced.
|
||||
*
|
||||
* The API will always be preferred if both its base URL and a token are
|
||||
* present.
|
||||
* Values omitted at the optional property at the app-config will be deduced
|
||||
* from the "host" value.
|
||||
*/
|
||||
apiBaseUrl?: string;
|
||||
apiBaseUrl: string;
|
||||
|
||||
/**
|
||||
* The authorization token to use for requests to a Bitbucket Server provider.
|
||||
@@ -90,6 +88,8 @@ export function readBitbucketIntegrationConfig(
|
||||
apiBaseUrl = trimEnd(apiBaseUrl, '/');
|
||||
} else if (host === BITBUCKET_HOST) {
|
||||
apiBaseUrl = BITBUCKET_API_BASE_URL;
|
||||
} else {
|
||||
apiBaseUrl = `https://${host}/rest/api/1.0`;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -24,7 +24,10 @@ import {
|
||||
describe('basicIntegrations', () => {
|
||||
describe('byUrl', () => {
|
||||
it('handles hosts without a port', () => {
|
||||
const integration = new BitbucketIntegration({ host: 'host.com' });
|
||||
const integration = new BitbucketIntegration({
|
||||
host: 'host.com',
|
||||
apiBaseUrl: 'a',
|
||||
});
|
||||
const integrations = basicIntegrations<BitbucketIntegration>(
|
||||
[integration],
|
||||
i => i.config.host,
|
||||
@@ -33,7 +36,10 @@ describe('basicIntegrations', () => {
|
||||
expect(integrations.byUrl('https://host.com:8080/a')).toBeUndefined();
|
||||
});
|
||||
it('handles hosts with a port', () => {
|
||||
const integration = new BitbucketIntegration({ host: 'host.com:8080' });
|
||||
const integration = new BitbucketIntegration({
|
||||
host: 'host.com:8080',
|
||||
apiBaseUrl: 'a',
|
||||
});
|
||||
const integrations = basicIntegrations<BitbucketIntegration>(
|
||||
[integration],
|
||||
i => i.config.host,
|
||||
|
||||
@@ -3,38 +3,33 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="node" />
|
||||
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { Permission } from '@backstage/plugin-permission-common';
|
||||
import { Readable } from 'stream';
|
||||
import { Transform } from 'stream';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DocumentCollator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export interface DocumentCollator {
|
||||
// (undocumented)
|
||||
execute(): Promise<IndexableDocument[]>;
|
||||
// @beta
|
||||
export interface DocumentCollatorFactory {
|
||||
getCollator(): Promise<Readable>;
|
||||
readonly type: string;
|
||||
readonly visibilityPermission?: Permission;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DocumentDecorator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export interface DocumentDecorator {
|
||||
// (undocumented)
|
||||
execute(documents: IndexableDocument[]): Promise<IndexableDocument[]>;
|
||||
// @beta
|
||||
export interface DocumentDecoratorFactory {
|
||||
getDecorator(): Promise<Transform>;
|
||||
readonly types?: string[];
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DocumentTypeInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
// @beta
|
||||
export type DocumentTypeInfo = {
|
||||
visibilityPermission?: Permission;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "IndexableDocument" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
// @beta
|
||||
export interface IndexableDocument {
|
||||
authorization?: {
|
||||
resourceRef: string;
|
||||
@@ -44,23 +39,17 @@ export interface IndexableDocument {
|
||||
title: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "QueryRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @beta
|
||||
export type QueryRequestOptions = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "QueryTranslator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
// @beta
|
||||
export type QueryTranslator = (query: SearchQuery) => unknown;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
// @beta
|
||||
export interface SearchEngine {
|
||||
index(type: string, documents: IndexableDocument[]): Promise<void>;
|
||||
getIndexer(type: string): Promise<Writable>;
|
||||
query(
|
||||
query: SearchQuery,
|
||||
options?: QueryRequestOptions,
|
||||
@@ -68,9 +57,7 @@ export interface SearchEngine {
|
||||
setTranslator(translator: QueryTranslator): void;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @beta (undocumented)
|
||||
export interface SearchQuery {
|
||||
// (undocumented)
|
||||
filters?: JsonObject;
|
||||
@@ -82,9 +69,7 @@ export interface SearchQuery {
|
||||
types?: string[];
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @beta (undocumented)
|
||||
export interface SearchResult {
|
||||
// (undocumented)
|
||||
document: IndexableDocument;
|
||||
@@ -92,9 +77,7 @@ export interface SearchResult {
|
||||
type: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchResultSet" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @beta (undocumented)
|
||||
export interface SearchResultSet {
|
||||
// (undocumented)
|
||||
nextPageCursor?: string;
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
import { Permission } from '@backstage/plugin-permission-common';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { Readable, Transform, Writable } from 'stream';
|
||||
|
||||
/**
|
||||
* @beta
|
||||
*/
|
||||
export interface SearchQuery {
|
||||
term: string;
|
||||
filters?: JsonObject;
|
||||
@@ -24,11 +28,17 @@ export interface SearchQuery {
|
||||
pageCursor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @beta
|
||||
*/
|
||||
export interface SearchResult {
|
||||
type: string;
|
||||
document: IndexableDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* @beta
|
||||
*/
|
||||
export interface SearchResultSet {
|
||||
results: SearchResult[];
|
||||
nextPageCursor?: string;
|
||||
@@ -38,6 +48,7 @@ export interface SearchResultSet {
|
||||
/**
|
||||
* Base properties that all indexed documents must include, as well as some
|
||||
* common properties that documents are encouraged to use where appropriate.
|
||||
* @beta
|
||||
*/
|
||||
export interface IndexableDocument {
|
||||
/**
|
||||
@@ -72,6 +83,7 @@ export interface IndexableDocument {
|
||||
* Information about a specific document type. Intended to be used in the
|
||||
* {@link @backstage/search-backend-node#IndexBuilder} to collect information
|
||||
* about the types stored in the index.
|
||||
* @beta
|
||||
*/
|
||||
export type DocumentTypeInfo = {
|
||||
/**
|
||||
@@ -82,10 +94,10 @@ export type DocumentTypeInfo = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface that must be implemented in order to expose new documents to
|
||||
* search.
|
||||
* Factory class for instantiating collators.
|
||||
* @beta
|
||||
*/
|
||||
export interface DocumentCollator {
|
||||
export interface DocumentCollatorFactory {
|
||||
/**
|
||||
* The type or name of the document set returned by this collator. Used as an
|
||||
* index name by Search Engines.
|
||||
@@ -98,29 +110,41 @@ export interface DocumentCollator {
|
||||
*/
|
||||
readonly visibilityPermission?: Permission;
|
||||
|
||||
execute(): Promise<IndexableDocument[]>;
|
||||
/**
|
||||
* Instantiates and resolves a document collator.
|
||||
*/
|
||||
getCollator(): Promise<Readable>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface that must be implemented in order to decorate existing documents with
|
||||
* additional metadata.
|
||||
* Factory class for instantiating decorators.
|
||||
* @beta
|
||||
*/
|
||||
export interface DocumentDecorator {
|
||||
export interface DocumentDecoratorFactory {
|
||||
/**
|
||||
* An optional array of document/index types on which this decorator should
|
||||
* be applied. If no types are provided, this decorator will be applied to
|
||||
* all document/index types.
|
||||
*/
|
||||
readonly types?: string[];
|
||||
execute(documents: IndexableDocument[]): Promise<IndexableDocument[]>;
|
||||
|
||||
/**
|
||||
* Instantiates and resolves a document decorator.
|
||||
*/
|
||||
getDecorator(): Promise<Transform>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type of function responsible for translating an abstract search query into
|
||||
* a concrete query relevant to a particular search engine.
|
||||
* @beta
|
||||
*/
|
||||
export type QueryTranslator = (query: SearchQuery) => unknown;
|
||||
|
||||
/**
|
||||
* Options when querying a search engine.
|
||||
* @beta
|
||||
*/
|
||||
export type QueryRequestOptions = {
|
||||
token?: string;
|
||||
};
|
||||
@@ -129,6 +153,7 @@ export type QueryRequestOptions = {
|
||||
* Interface that must be implemented by specific search engines, responsible
|
||||
* for performing indexing and querying and translating abstract queries into
|
||||
* concrete, search engine-specific queries.
|
||||
* @beta
|
||||
*/
|
||||
export interface SearchEngine {
|
||||
/**
|
||||
@@ -137,9 +162,15 @@ export interface SearchEngine {
|
||||
setTranslator(translator: QueryTranslator): void;
|
||||
|
||||
/**
|
||||
* Add the given documents to the SearchEngine index of the given type.
|
||||
* Factory method for getting a search engine indexer for a given document
|
||||
* type.
|
||||
*
|
||||
* @param type - The type or name of the document set for which an indexer
|
||||
* should be retrieved. This corresponds to the `type` property on the
|
||||
* document collator/decorator factories and will most often be used to
|
||||
* identify an index or group to which documents should be written.
|
||||
*/
|
||||
index(type: string, documents: IndexableDocument[]): Promise<void>;
|
||||
getIndexer(type: string): Promise<Writable>;
|
||||
|
||||
/**
|
||||
* Perform a search query against the SearchEngine.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
scmIntegrationsApiRef,
|
||||
@@ -81,7 +81,7 @@ class TechDocsDevStorageApi implements TechDocsStorageApi {
|
||||
return this.configApi.getString('techdocs.builder');
|
||||
}
|
||||
|
||||
async getEntityDocs(_entityId: EntityName, path: string) {
|
||||
async getEntityDocs(_entityId: CompoundEntityRef, path: string) {
|
||||
const apiOrigin = await this.getApiOrigin();
|
||||
// Irrespective of the entity, use mkdocs server to find the file for the path.
|
||||
const url = `${apiOrigin}/${path}`;
|
||||
@@ -97,7 +97,7 @@ class TechDocsDevStorageApi implements TechDocsStorageApi {
|
||||
return request.text();
|
||||
}
|
||||
|
||||
async syncEntityDocs(_: EntityName): Promise<SyncResult> {
|
||||
async syncEntityDocs(_: CompoundEntityRef): Promise<SyncResult> {
|
||||
// this is just stub of this function as we don't need to check if docs are up to date,
|
||||
// we always want to retrigger a new build
|
||||
return 'cached';
|
||||
@@ -106,7 +106,7 @@ class TechDocsDevStorageApi implements TechDocsStorageApi {
|
||||
// Used by transformer to modify the request to assets (CSS, Image) from inside the HTML.
|
||||
async getBaseUrl(
|
||||
oldBaseUrl: string,
|
||||
_entityId: EntityName,
|
||||
_entityId: CompoundEntityRef,
|
||||
path: string,
|
||||
): Promise<string> {
|
||||
const apiOrigin = await this.getApiOrigin();
|
||||
@@ -154,7 +154,7 @@ class TechDocsDevApi implements TechDocsApi {
|
||||
};
|
||||
}
|
||||
|
||||
async getTechDocsMetadata(_entityId: EntityName) {
|
||||
async getTechDocsMetadata(_entityId: CompoundEntityRef) {
|
||||
return {
|
||||
site_name: 'Live preview environment',
|
||||
site_description: '',
|
||||
|
||||
@@ -29,7 +29,7 @@ import LightIcon from '@material-ui/icons/Brightness7';
|
||||
import DarkIcon from '@material-ui/icons/Brightness4';
|
||||
|
||||
import { lightTheme, darkTheme } from '@backstage/theme';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
import { Content } from '@backstage/core-components';
|
||||
|
||||
@@ -127,7 +127,7 @@ const TechDocsPageContent = ({
|
||||
onReady,
|
||||
entityRef,
|
||||
}: {
|
||||
entityRef: EntityName;
|
||||
entityRef: CompoundEntityRef;
|
||||
onReady: () => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
@@ -39,6 +39,7 @@ export default async function generate(cmd: Command) {
|
||||
|
||||
const sourceDir = resolve(cmd.sourceDir);
|
||||
const outputDir = resolve(cmd.outputDir);
|
||||
const omitTechdocsCorePlugin = cmd.omitTechdocsCoreMkdocsPlugin;
|
||||
const dockerImage = cmd.dockerImage;
|
||||
const pullImage = cmd.pull;
|
||||
|
||||
@@ -55,6 +56,9 @@ export default async function generate(cmd: Command) {
|
||||
runIn: cmd.docker ? 'docker' : 'local',
|
||||
dockerImage,
|
||||
pullImage,
|
||||
mkdocs: {
|
||||
omitTechdocsCorePlugin,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,6 +54,11 @@ export function registerCommands(program: CommanderStatic) {
|
||||
'A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json.',
|
||||
)
|
||||
.option('-v --verbose', 'Enable verbose output.', false)
|
||||
.option(
|
||||
'--omitTechdocsCoreMkdocsPlugin',
|
||||
"Don't patch MkDocs file automatically with techdocs-core plugin.",
|
||||
false,
|
||||
)
|
||||
.alias('build')
|
||||
.action(lazy(() => import('./generate/generate').then(m => m.default)));
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
```ts
|
||||
/// <reference types="node" />
|
||||
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ContainerRunner } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import express from 'express';
|
||||
import { IndexableDocument } from '@backstage/search-common';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
@@ -157,7 +157,9 @@ export class Publisher {
|
||||
// @public
|
||||
export interface PublisherBase {
|
||||
docsRouter(): express.Handler;
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata>;
|
||||
fetchTechDocsMetadata(
|
||||
entityName: CompoundEntityRef,
|
||||
): Promise<TechDocsMetadata>;
|
||||
getReadiness(): Promise<ReadinessResponse>;
|
||||
hasDocsBeenGenerated(entityName: Entity): Promise<boolean>;
|
||||
migrateDocsCase?(migrateRequest: MigrateRequest): Promise<void>;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
site_name: Test site name
|
||||
site_description: Test site description
|
||||
docs_dir: docs/
|
||||
plugins:
|
||||
- not-techdocs-core
|
||||
- also-not-techdocs-core
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
site_name: Test site name
|
||||
site_description: Test site description
|
||||
# This is a comment that is removed after editing
|
||||
plugins:
|
||||
- techdocs-core
|
||||
@@ -0,0 +1,3 @@
|
||||
site_name: Test site name
|
||||
site_description: Test site description
|
||||
docs_dir: docs/
|
||||
@@ -28,10 +28,14 @@ import {
|
||||
getMkdocsYml,
|
||||
getRepoUrlFromLocationAnnotation,
|
||||
patchIndexPreBuild,
|
||||
patchMkdocsYmlPreBuild,
|
||||
storeEtagMetadata,
|
||||
validateMkdocsYaml,
|
||||
} from './helpers';
|
||||
import {
|
||||
patchMkdocsYmlPreBuild,
|
||||
pathMkdocsYmlWithTechdocsPlugin,
|
||||
} from './mkDocsPatchers';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
const mockEntity = {
|
||||
apiVersion: 'version',
|
||||
@@ -65,6 +69,15 @@ const mkdocsYmlWithInvalidDocDir2 = fs.readFileSync(
|
||||
const mkdocsYmlWithComments = fs.readFileSync(
|
||||
resolvePath(__filename, '../__fixtures__/mkdocs_with_comments.yml'),
|
||||
);
|
||||
const mkdocsYmlWithTechdocsPlugins = fs.readFileSync(
|
||||
resolvePath(__filename, '../__fixtures__/mkdocs_with_techdocs_plugin.yml'),
|
||||
);
|
||||
const mkdocsYmlWithoutPlugins = fs.readFileSync(
|
||||
resolvePath(__filename, '../__fixtures__/mkdocs_without_plugins.yml'),
|
||||
);
|
||||
const mkdocsYmlWithAdditionalPlugins = fs.readFileSync(
|
||||
resolvePath(__filename, '../__fixtures__/mkdocs_with_additional_plugins.yml'),
|
||||
);
|
||||
const mockLogger = getVoidLogger();
|
||||
const warn = jest.spyOn(mockLogger, 'warn');
|
||||
|
||||
@@ -289,6 +302,60 @@ describe('helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('pathMkdocsYmlWithTechdocsPlugin', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins,
|
||||
'/mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins,
|
||||
'/mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins,
|
||||
});
|
||||
});
|
||||
it('should not add additional plugins if techdocs exists already in mkdocs file', async () => {
|
||||
await pathMkdocsYmlWithTechdocsPlugin(
|
||||
'/mkdocs_with_techdocs_plugin.yml',
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
'/mkdocs_with_techdocs_plugin.yml',
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
};
|
||||
expect(parsedYml.plugins).toHaveLength(1);
|
||||
expect(parsedYml.plugins).toContain('techdocs-core');
|
||||
});
|
||||
it("should add the needed plugin if it doesn't exist in mkdocs file", async () => {
|
||||
await pathMkdocsYmlWithTechdocsPlugin(
|
||||
'/mkdocs_without_plugins.yml',
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile('/mkdocs_without_plugins.yml');
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
};
|
||||
expect(parsedYml.plugins).toHaveLength(1);
|
||||
expect(parsedYml.plugins).toContain('techdocs-core');
|
||||
});
|
||||
it('should not override existing plugins', async () => {
|
||||
await pathMkdocsYmlWithTechdocsPlugin(
|
||||
'/mkdocs_with_additional_plugins.yml',
|
||||
mockLogger,
|
||||
);
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
'/mkdocs_with_additional_plugins.yml',
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
};
|
||||
expect(parsedYml.plugins).toHaveLength(3);
|
||||
expect(parsedYml.plugins).toContain('techdocs-core');
|
||||
expect(parsedYml.plugins).toContain('not-techdocs-core');
|
||||
expect(parsedYml.plugins).toContain('also-not-techdocs-core');
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchIndexPreBuild', () => {
|
||||
afterEach(() => {
|
||||
warn.mockClear();
|
||||
|
||||
@@ -125,7 +125,7 @@ class UnknownTag {
|
||||
constructor(public readonly data: any, public readonly type?: string) {}
|
||||
}
|
||||
|
||||
const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
export const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
new Type('', {
|
||||
kind: 'scalar',
|
||||
multi: true,
|
||||
@@ -203,101 +203,6 @@ export const validateMkdocsYaml = async (
|
||||
return parsedMkdocsYml.docs_dir;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the mkdocs.yml file before TechDocs generator uses it to generate docs site.
|
||||
*
|
||||
* List of tasks:
|
||||
* - Add repo_url or edit_uri if it does not exists
|
||||
* If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default.
|
||||
* If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get
|
||||
* the repository URL.
|
||||
*
|
||||
* This function will not throw an error since this is not critical to the whole TechDocs pipeline.
|
||||
* Instead it will log warnings if there are any errors in reading, parsing or writing YAML.
|
||||
*
|
||||
* @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site
|
||||
* @param logger - A logger instance
|
||||
* @param parsedLocationAnnotation - Object with location url and type
|
||||
* @param scmIntegrations - the scmIntegration to do url transformations
|
||||
*/
|
||||
export const patchMkdocsYmlPreBuild = async (
|
||||
mkdocsYmlPath: string,
|
||||
logger: Logger,
|
||||
parsedLocationAnnotation: ParsedLocationAnnotation,
|
||||
scmIntegrations: ScmIntegrationRegistry,
|
||||
) => {
|
||||
// We only want to override the mkdocs.yml if it has actually changed. This is relevant if
|
||||
// used with a 'dir' location on the file system as this would permanently update the file.
|
||||
let didEdit = false;
|
||||
|
||||
let mkdocsYmlFileString;
|
||||
try {
|
||||
mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8');
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mkdocsYml: any;
|
||||
try {
|
||||
mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA });
|
||||
|
||||
// mkdocsYml should be an object type after successful parsing.
|
||||
// But based on its type definition, it can also be a string or undefined, which we don't want.
|
||||
if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') {
|
||||
throw new Error('Bad YAML format.');
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add edit_uri and/or repo_url to mkdocs.yml if it is missing.
|
||||
// This will enable the Page edit button generated by MkDocs.
|
||||
// If the either has been set, keep the original value
|
||||
if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) {
|
||||
const result = getRepoUrlFromLocationAnnotation(
|
||||
parsedLocationAnnotation,
|
||||
scmIntegrations,
|
||||
mkdocsYml.docs_dir,
|
||||
);
|
||||
|
||||
if (result.repo_url || result.edit_uri) {
|
||||
mkdocsYml.repo_url = result.repo_url;
|
||||
mkdocsYml.edit_uri = result.edit_uri;
|
||||
didEdit = true;
|
||||
|
||||
logger.info(
|
||||
`Set ${JSON.stringify(
|
||||
result,
|
||||
)}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (didEdit) {
|
||||
await fs.writeFile(
|
||||
mkdocsYmlPath,
|
||||
yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update docs/index.md file before TechDocs generator uses it to generate docs site,
|
||||
* falling back to docs/README.md or README.md in case a default docs/index.md
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Logger } from 'winston';
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'js-yaml';
|
||||
import { ParsedLocationAnnotation } from '../../helpers';
|
||||
import { getRepoUrlFromLocationAnnotation, MKDOCS_SCHEMA } from './helpers';
|
||||
import { assertError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
|
||||
type MkDocsObject = {
|
||||
plugins?: string[];
|
||||
docs_dir: string;
|
||||
repo_url?: string;
|
||||
edit_uri?: string;
|
||||
};
|
||||
|
||||
const patchMkdocsFile = async (
|
||||
mkdocsYmlPath: string,
|
||||
logger: Logger,
|
||||
updateAction: (mkdocsYml: MkDocsObject) => boolean,
|
||||
) => {
|
||||
// We only want to override the mkdocs.yml if it has actually changed. This is relevant if
|
||||
// used with a 'dir' location on the file system as this would permanently update the file.
|
||||
let didEdit = false;
|
||||
|
||||
let mkdocsYmlFileString;
|
||||
try {
|
||||
mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8');
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mkdocsYml: any;
|
||||
try {
|
||||
mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA });
|
||||
|
||||
// mkdocsYml should be an object type after successful parsing.
|
||||
// But based on its type definition, it can also be a string or undefined, which we don't want.
|
||||
if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') {
|
||||
throw new Error('Bad YAML format.');
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
didEdit = updateAction(mkdocsYml);
|
||||
|
||||
try {
|
||||
if (didEdit) {
|
||||
await fs.writeFile(
|
||||
mkdocsYmlPath,
|
||||
yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
logger.warn(
|
||||
`Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the mkdocs.yml file before TechDocs generator uses it to generate docs site.
|
||||
*
|
||||
* List of tasks:
|
||||
* - Add repo_url or edit_uri if it does not exists
|
||||
* If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default.
|
||||
* If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get
|
||||
* the repository URL.
|
||||
*
|
||||
* This function will not throw an error since this is not critical to the whole TechDocs pipeline.
|
||||
* Instead it will log warnings if there are any errors in reading, parsing or writing YAML.
|
||||
*
|
||||
* @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site
|
||||
* @param logger - A logger instance
|
||||
* @param parsedLocationAnnotation - Object with location url and type
|
||||
* @param scmIntegrations - the scmIntegration to do url transformations
|
||||
*/
|
||||
export const patchMkdocsYmlPreBuild = async (
|
||||
mkdocsYmlPath: string,
|
||||
logger: Logger,
|
||||
parsedLocationAnnotation: ParsedLocationAnnotation,
|
||||
scmIntegrations: ScmIntegrationRegistry,
|
||||
) => {
|
||||
await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => {
|
||||
if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) {
|
||||
// Add edit_uri and/or repo_url to mkdocs.yml if it is missing.
|
||||
// This will enable the Page edit button generated by MkDocs.
|
||||
// If the either has been set, keep the original value
|
||||
const result = getRepoUrlFromLocationAnnotation(
|
||||
parsedLocationAnnotation,
|
||||
scmIntegrations,
|
||||
mkdocsYml.docs_dir,
|
||||
);
|
||||
|
||||
if (result.repo_url || result.edit_uri) {
|
||||
mkdocsYml.repo_url = result.repo_url;
|
||||
mkdocsYml.edit_uri = result.edit_uri;
|
||||
|
||||
logger.info(
|
||||
`Set ${JSON.stringify(
|
||||
result,
|
||||
)}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the mkdocs.yml file before TechDocs generator uses it to generate docs site.
|
||||
*
|
||||
* List of tasks:
|
||||
* - Add techdocs-core plugin to mkdocs file if it doesn't exist
|
||||
*
|
||||
* This function will not throw an error since this is not critical to the whole TechDocs pipeline.
|
||||
* Instead it will log warnings if there are any errors in reading, parsing or writing YAML.
|
||||
*
|
||||
* @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site
|
||||
* @param logger - A logger instance
|
||||
*/
|
||||
export const pathMkdocsYmlWithTechdocsPlugin = async (
|
||||
mkdocsYmlPath: string,
|
||||
logger: Logger,
|
||||
) => {
|
||||
await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => {
|
||||
// Modify mkdocs.yaml to contain the needed techdocs-core plugin if it is not there
|
||||
if (!('plugins' in mkdocsYml)) {
|
||||
mkdocsYml.plugins = ['techdocs-core'];
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mkdocsYml.plugins && !mkdocsYml.plugins.includes('techdocs-core')) {
|
||||
mkdocsYml.plugins.push('techdocs-core');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
@@ -26,11 +26,15 @@ import {
|
||||
createOrUpdateMetadata,
|
||||
getMkdocsYml,
|
||||
patchIndexPreBuild,
|
||||
patchMkdocsYmlPreBuild,
|
||||
runCommand,
|
||||
storeEtagMetadata,
|
||||
validateMkdocsYaml,
|
||||
} from './helpers';
|
||||
|
||||
import {
|
||||
patchMkdocsYmlPreBuild,
|
||||
pathMkdocsYmlWithTechdocsPlugin,
|
||||
} from './mkDocsPatchers';
|
||||
import {
|
||||
GeneratorBase,
|
||||
GeneratorConfig,
|
||||
@@ -110,6 +114,10 @@ export class TechdocsGenerator implements GeneratorBase {
|
||||
await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir });
|
||||
}
|
||||
|
||||
if (!this.options.omitTechdocsCoreMkdocsPlugin) {
|
||||
await pathMkdocsYmlWithTechdocsPlugin(mkdocsYmlPath, childLogger);
|
||||
}
|
||||
|
||||
// Directories to bind on container
|
||||
const mountDirs = {
|
||||
[inputDir]: '/input',
|
||||
@@ -207,5 +215,8 @@ export function readGeneratorConfig(
|
||||
'docker',
|
||||
dockerImage: config.getOptionalString('techdocs.generator.dockerImage'),
|
||||
pullImage: config.getOptionalBoolean('techdocs.generator.pullImage'),
|
||||
omitTechdocsCoreMkdocsPlugin: config.getOptionalBoolean(
|
||||
'techdocs.generator.mkdocs.omitTechdocsCorePlugin',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export type GeneratorConfig = {
|
||||
runIn: GeneratorRunInType;
|
||||
dockerImage?: string;
|
||||
pullImage?: boolean;
|
||||
omitTechdocsCoreMkdocsPlugin?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { assertError, ForwardedError } from '@backstage/errors';
|
||||
import aws, { Credentials } from 'aws-sdk';
|
||||
@@ -321,7 +321,7 @@ export class AwsS3Publish implements PublisherBase {
|
||||
}
|
||||
|
||||
async fetchTechDocsMetadata(
|
||||
entityName: EntityName,
|
||||
entityName: CompoundEntityRef,
|
||||
): Promise<TechDocsMetadata> {
|
||||
try {
|
||||
return await new Promise<TechDocsMetadata>(async (resolve, reject) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
ContainerClient,
|
||||
StorageSharedKeyCredential,
|
||||
} from '@azure/storage-blob';
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { assertError, ForwardedError } from '@backstage/errors';
|
||||
import express from 'express';
|
||||
@@ -300,7 +300,7 @@ export class AzureBlobStoragePublish implements PublisherBase {
|
||||
}
|
||||
|
||||
async fetchTechDocsMetadata(
|
||||
entityName: EntityName,
|
||||
entityName: CompoundEntityRef,
|
||||
): Promise<TechDocsMetadata> {
|
||||
const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
|
||||
const entityRootDir = this.legacyPathCasing
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { assertError } from '@backstage/errors';
|
||||
import { File, FileExistsResponse, Storage } from '@google-cloud/storage';
|
||||
@@ -238,7 +238,9 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
return { objects };
|
||||
}
|
||||
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata> {
|
||||
fetchTechDocsMetadata(
|
||||
entityName: CompoundEntityRef,
|
||||
): Promise<TechDocsMetadata> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
|
||||
const entityDir = this.legacyPathCasing
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
PluginEndpointDiscovery,
|
||||
resolvePackagePath,
|
||||
} from '@backstage/backend-common';
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import fs from 'fs-extra';
|
||||
@@ -142,7 +142,7 @@ export class LocalPublish implements PublisherBase {
|
||||
}
|
||||
|
||||
async fetchTechDocsMetadata(
|
||||
entityName: EntityName,
|
||||
entityName: CompoundEntityRef,
|
||||
): Promise<TechDocsMetadata> {
|
||||
const metadataPath = this.staticEntityPathJoin(
|
||||
entityName.namespace,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
Entity,
|
||||
EntityName,
|
||||
CompoundEntityRef,
|
||||
DEFAULT_NAMESPACE,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
@@ -45,7 +45,7 @@ const createMockEntity = (annotations = {}): Entity => {
|
||||
};
|
||||
};
|
||||
|
||||
const createMockEntityName = (): EntityName => ({
|
||||
const createMockEntityName = (): CompoundEntityRef => ({
|
||||
kind: 'TestKind',
|
||||
name: 'test-component-name',
|
||||
namespace: 'test-namespace',
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import fs from 'fs-extra';
|
||||
@@ -194,7 +194,7 @@ export class OpenStackSwiftPublish implements PublisherBase {
|
||||
}
|
||||
|
||||
async fetchTechDocsMetadata(
|
||||
entityName: EntityName,
|
||||
entityName: CompoundEntityRef,
|
||||
): Promise<TechDocsMetadata> {
|
||||
return await new Promise<TechDocsMetadata>(async (resolve, reject) => {
|
||||
const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { Logger } from 'winston';
|
||||
import express from 'express';
|
||||
@@ -133,7 +133,9 @@ export interface PublisherBase {
|
||||
* Retrieve TechDocs Metadata about a site e.g. name, contributors, last updated, etc.
|
||||
* This API uses the techdocs_metadata.json file that co-exists along with the generated docs.
|
||||
*/
|
||||
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata>;
|
||||
fetchTechDocsMetadata(
|
||||
entityName: CompoundEntityRef,
|
||||
): Promise<TechDocsMetadata>;
|
||||
|
||||
/**
|
||||
* Route middleware to serve static documentation files for an entity.
|
||||
|
||||
@@ -24,7 +24,7 @@ This method of serving the plugin provides quicker iteration speed and a faster
|
||||
|
||||
3. Go into the plugin's directory and run it in standalone mode by running `yarn start`.
|
||||
|
||||
Access it from http://localhost:7007/api/airbrake. Or use the Airbrake plugin which will talk to it automatically.
|
||||
Access it from http://localhost:7007/api/airbrake. Or use the [Airbrake plugin in standalone mode](../airbrake/README.md#local-development) which will talk to it automatically.
|
||||
|
||||
Here are some example endpoints:
|
||||
|
||||
|
||||
+51
-13
@@ -20,7 +20,7 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https:
|
||||
yarn add @backstage/plugin-airbrake-backend
|
||||
```
|
||||
|
||||
3. Add the `EntityAirbrakeContent` to `packages/app/src/components/catalog/EntityPage.tsx`:
|
||||
3. Add the `EntityAirbrakeContent` to `packages/app/src/components/catalog/EntityPage.tsx` for all the entity pages you want Airbrake to be in:
|
||||
|
||||
```typescript jsx
|
||||
import { EntityAirbrakeContent } from '@backstage/plugin-airbrake';
|
||||
@@ -32,40 +32,78 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https:
|
||||
</EntityLayout.Route>
|
||||
</EntityLayoutWrapper>
|
||||
);
|
||||
|
||||
const websiteEntityPage = (
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout.Route path="/airbrake" title="Airbrake">
|
||||
<EntityAirbrakeContent />
|
||||
</EntityLayout.Route>
|
||||
</EntityLayoutWrapper>
|
||||
);
|
||||
|
||||
const defaultEntityPage = (
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout.Route path="/airbrake" title="Airbrake">
|
||||
<EntityAirbrakeContent />
|
||||
</EntityLayout.Route>
|
||||
</EntityLayoutWrapper>
|
||||
);
|
||||
```
|
||||
|
||||
4. Setup the Backend code in `packages/backend/src/index.ts`:
|
||||
4. Create `packages/backend/src/plugins/airbrake.ts` with these contents:
|
||||
|
||||
```typescript
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import {
|
||||
createRouter as createAirbrakeRouter,
|
||||
createRouter,
|
||||
extractAirbrakeConfig,
|
||||
} from '@backstage/plugin-airbrake-backend';
|
||||
|
||||
async function main() {
|
||||
//... After const config = await loadBackendConfig({ ...
|
||||
|
||||
const airbrakeRouter = await createAirbrakeRouter({
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return createRouter({
|
||||
logger,
|
||||
airbrakeConfig: extractAirbrakeConfig(config),
|
||||
});
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
// ... Add the airbrakeRouter here
|
||||
.addRouter('/api/airbrake', airbrakeRouter);
|
||||
}
|
||||
```
|
||||
|
||||
5. Add this config as a top level section in your `app-config.yaml`:
|
||||
5. Setup the Backend code in `packages/backend/src/index.ts`:
|
||||
|
||||
```typescript
|
||||
import airbrake from './plugins/airbrake';
|
||||
|
||||
async function main() {
|
||||
//... After const createEnv = makeCreateEnv(config) ...
|
||||
|
||||
const airbrakeEnv = useHotMemoize(module, () => createEnv('airbrake'));
|
||||
|
||||
//... After const apiRouter = Router() ...
|
||||
apiRouter.use('/airbrake', await airbrake(airbrakeEnv));
|
||||
}
|
||||
```
|
||||
|
||||
6. Add this config as a top level section in your `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
airbrake:
|
||||
apiKey: ${AIRBRAKE_API_KEY}
|
||||
```
|
||||
|
||||
6. Set an environment variable `AIRBRAKE_API_KEY` with your [API key](https://airbrake.io/docs/api/#authentication)
|
||||
7. Set an environment variable `AIRBRAKE_API_KEY` with your [API key](https://airbrake.io/docs/api/#authentication)
|
||||
before starting Backstage backend.
|
||||
|
||||
8. Add the following annotation to the `catalog-info.yaml` for a repo you want to link to an Airbrake project:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
annotations:
|
||||
airbrake.io/project-id: '123456'
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
Start this plugin in standalone mode by running `yarn start` inside the plugin directory. This method of serving the plugin provides quicker
|
||||
|
||||
@@ -53,8 +53,8 @@ createDevApp()
|
||||
items: mockEntities.slice(),
|
||||
};
|
||||
},
|
||||
async getEntityByName(name: string) {
|
||||
return mockEntities.find(e => e.metadata.name === name);
|
||||
async getEntityByRef(ref: string) {
|
||||
return mockEntities.find(e => e.metadata.name === ref);
|
||||
},
|
||||
} as unknown as typeof catalogApiRef.T),
|
||||
})
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, RELATION_MEMBER_OF } from '@backstage/catalog-model';
|
||||
import {
|
||||
Entity,
|
||||
parseEntityRef,
|
||||
RELATION_MEMBER_OF,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { TableColumn, TableProps } from '@backstage/core-components';
|
||||
import {
|
||||
@@ -60,11 +64,11 @@ describe('DefaultApiExplorerPage', () => {
|
||||
}),
|
||||
getLocationByRef: () =>
|
||||
Promise.resolve({ id: 'id', type: 'url', target: 'url' }),
|
||||
getEntityByName: async entityName => {
|
||||
getEntityByRef: async entityRef => {
|
||||
return {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: { name: entityName.name },
|
||||
metadata: { name: parseEntityRef(entityRef).name },
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { MemoryKeyStore } from './MemoryKeyStore';
|
||||
import { TokenFactory } from './TokenFactory';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { JWKS, JSONWebKey, JWT } from 'jose';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
@@ -28,6 +29,12 @@ function jwtKid(jwt: string): string {
|
||||
return header.kid;
|
||||
}
|
||||
|
||||
const entityRef = stringifyEntityRef({
|
||||
kind: 'User',
|
||||
namespace: 'default',
|
||||
name: 'JackFrost',
|
||||
});
|
||||
|
||||
describe('TokenFactory', () => {
|
||||
it('should issue valid tokens signed by a listed key', async () => {
|
||||
const keyDurationSeconds = 5;
|
||||
@@ -39,7 +46,7 @@ describe('TokenFactory', () => {
|
||||
});
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({ keys: [] });
|
||||
const token = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const token = await factory.issueToken({ claims: { sub: entityRef } });
|
||||
|
||||
const { keys } = await factory.listPublicKeys();
|
||||
const keyStore = JWKS.asKeyStore({
|
||||
@@ -53,7 +60,7 @@ describe('TokenFactory', () => {
|
||||
expect(payload).toEqual({
|
||||
iss: 'my-issuer',
|
||||
aud: 'backstage',
|
||||
sub: 'foo',
|
||||
sub: entityRef,
|
||||
iat: expect.any(Number),
|
||||
exp: expect.any(Number),
|
||||
});
|
||||
@@ -71,8 +78,12 @@ describe('TokenFactory', () => {
|
||||
logger,
|
||||
});
|
||||
|
||||
const token1 = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const token2 = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const token1 = await factory.issueToken({
|
||||
claims: { sub: entityRef },
|
||||
});
|
||||
const token2 = await factory.issueToken({
|
||||
claims: { sub: entityRef },
|
||||
});
|
||||
expect(jwtKid(token1)).toBe(jwtKid(token2));
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({
|
||||
@@ -89,7 +100,9 @@ describe('TokenFactory', () => {
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const token3 = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const token3 = await factory.issueToken({
|
||||
claims: { sub: entityRef },
|
||||
});
|
||||
expect(jwtKid(token3)).not.toBe(jwtKid(token2));
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({
|
||||
@@ -100,4 +113,20 @@ describe('TokenFactory', () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error with a non entityRef sub claim', async () => {
|
||||
const keyDurationSeconds = 5;
|
||||
const factory = new TokenFactory({
|
||||
issuer: 'my-issuer',
|
||||
keyStore: new MemoryKeyStore(),
|
||||
keyDurationSeconds,
|
||||
logger,
|
||||
});
|
||||
|
||||
await expect(() => {
|
||||
return factory.issueToken({
|
||||
claims: { sub: 'UserId' },
|
||||
});
|
||||
}).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import { JSONWebKey, JWK, JWS } from 'jose';
|
||||
import { Logger } from 'winston';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DateTime } from 'luxon';
|
||||
import { parseEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
const MS_IN_S = 1000;
|
||||
|
||||
@@ -72,6 +73,15 @@ export class TokenFactory implements TokenIssuer {
|
||||
const iat = Math.floor(Date.now() / MS_IN_S);
|
||||
const exp = iat + this.keyDurationSeconds;
|
||||
|
||||
// Validate that the subject claim is a valid EntityRef
|
||||
try {
|
||||
parseEntityRef(sub);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
'"sub" claim provided by the auth resolver is not a valid EntityRef.',
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.info(`Issuing token for ${sub}, with entities ${ent ?? []}`);
|
||||
|
||||
return JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { CatalogIdentityClient } from './CatalogIdentityClient';
|
||||
describe('CatalogIdentityClient', () => {
|
||||
const catalogApi: jest.Mocked<CatalogApi> = {
|
||||
getLocationById: jest.fn(),
|
||||
getEntityByRef: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
getEntities: jest.fn(),
|
||||
addLocation: jest.fn(),
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Logger } from 'winston';
|
||||
import { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import {
|
||||
EntityName,
|
||||
CompoundEntityRef,
|
||||
parseEntityRef,
|
||||
RELATION_MEMBER_OF,
|
||||
stringifyEntityRef,
|
||||
@@ -96,7 +96,7 @@ export class CatalogIdentityClient {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((ref): ref is EntityName => ref !== null);
|
||||
.filter((ref): ref is CompoundEntityRef => ref !== null);
|
||||
|
||||
const filter = resolvedEntityRefs.map(ref => ({
|
||||
kind: ref.kind,
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('createRouter', () => {
|
||||
catalog = {
|
||||
addLocation: jest.fn(),
|
||||
getEntities: jest.fn(),
|
||||
getEntityByRef: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
getLocationByRef: jest.fn(),
|
||||
getLocationById: jest.fn(),
|
||||
@@ -103,7 +104,7 @@ describe('createRouter', () => {
|
||||
|
||||
describe('GET /entity/:namespace/:kind/:name/badge-specs', () => {
|
||||
it('returns all badge specs for entity', async () => {
|
||||
catalog.getEntityByName.mockResolvedValueOnce(entity);
|
||||
catalog.getEntityByRef.mockResolvedValueOnce(entity);
|
||||
|
||||
badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
|
||||
badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
|
||||
@@ -115,8 +116,8 @@ describe('createRouter', () => {
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.text).toEqual(JSON.stringify([badge], null, 2));
|
||||
|
||||
expect(catalog.getEntityByName).toHaveBeenCalledTimes(1);
|
||||
expect(catalog.getEntityByName).toHaveBeenCalledWith(
|
||||
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
|
||||
expect(catalog.getEntityByRef).toHaveBeenCalledWith(
|
||||
{
|
||||
namespace: 'default',
|
||||
kind: 'service',
|
||||
@@ -142,7 +143,7 @@ describe('createRouter', () => {
|
||||
|
||||
describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => {
|
||||
it('returns badge for entity', async () => {
|
||||
catalog.getEntityByName.mockResolvedValueOnce(entity);
|
||||
catalog.getEntityByRef.mockResolvedValueOnce(entity);
|
||||
|
||||
const image = '<svg>...</svg>';
|
||||
badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
|
||||
@@ -154,8 +155,8 @@ describe('createRouter', () => {
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(Buffer.from(image));
|
||||
|
||||
expect(catalog.getEntityByName).toHaveBeenCalledTimes(1);
|
||||
expect(catalog.getEntityByName).toHaveBeenCalledWith(
|
||||
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
|
||||
expect(catalog.getEntityByRef).toHaveBeenCalledWith(
|
||||
{
|
||||
namespace: 'default',
|
||||
kind: 'service',
|
||||
@@ -179,7 +180,7 @@ describe('createRouter', () => {
|
||||
});
|
||||
|
||||
it('returns badge spec for entity', async () => {
|
||||
catalog.getEntityByName.mockResolvedValueOnce(entity);
|
||||
catalog.getEntityByRef.mockResolvedValueOnce(entity);
|
||||
badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
|
||||
|
||||
const url = '/entity/default/service/test/badge/test-badge?format=json';
|
||||
@@ -192,7 +193,7 @@ describe('createRouter', () => {
|
||||
|
||||
describe('Errors', () => {
|
||||
it('returns 404 for unknown entities', async () => {
|
||||
catalog.getEntityByName.mockResolvedValue(undefined);
|
||||
catalog.getEntityByRef.mockResolvedValue(undefined);
|
||||
async function testUrl(url: string) {
|
||||
const response = await request(app).get(url);
|
||||
expect(response.status).toEqual(404);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user