Merge branch 'master' into feat/customSearchModal

This commit is contained in:
Emma Indal
2022-04-12 13:07:13 +02:00
committed by GitHub
218 changed files with 6456 additions and 1533 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-home': patch
---
Export template logos `TemplateBackstageLogo` and `TemplateBackstageLogoIcon` from package.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
imports from `@backstage/plugin-search-react` instead of `@backstage/plugin-search`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Bump the `rushstack` api generator libraries to their latest versions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Use new `PermissionEvaluator#authorizeConditional` method when retrieving permission conditions.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': patch
---
build(deps): bump `cronstrue` from 1.125.0 to 2.2.0
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kafka-backend': patch
---
build(deps-dev): bump `@types/jest-when` from 2.7.2 to 3.5.0
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
build(deps): bump `npm-packlist` from 3.0.0 to 5.0.0
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend': patch
---
Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Changed input label for owner field in GitlabRepoPicker
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-permission-common': patch
---
Added `PermissionEvaluator`, which will replace the existing `PermissionAuthorizer` interface. This new interface provides stronger type safety and validation by splitting `PermissionAuthorizer.authorize()` into two methods:
- `authorize()`: Used when the caller requires a definitive decision.
- `authorizeConditional()`: Used when the caller can optimize the evaluation of any conditional decisions. For example, a plugin backend may want to use conditions in a database query instead of evaluating each resource in memory.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
Now plugin configuration accept a new optional parameter `groupSelect` which allow the client to fetch defined fields from the ms-graph api.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-permission-node': minor
---
**BREAKING:** `ServerPermissionClient` now implements `PermissionEvaluator`, which moves out the capabilities for evaluating conditional decisions from `authorize()` to `authorizeConditional()` method.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-jenkins-backend': patch
---
Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated.
+76
View File
@@ -0,0 +1,76 @@
---
'@backstage/plugin-catalog-backend-module-aws': patch
---
Add a new provider `AwsS3EntityProvider` as replacement for `AwsS3DiscoveryProcessor`.
In order to migrate from the `AwsS3DiscoveryProcessor` you need to apply
the following changes:
**Before:**
```yaml
# app-config.yaml
catalog:
locations:
- type: s3-discovery
target: https://sample-bucket.s3.us-east-2.amazonaws.com/prefix/
```
```ts
/* packages/backend/src/plugins/catalog.ts */
import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-aws';
const builder = await CatalogBuilder.create(env);
/** ... other processors ... */
builder.addProcessor(new AwsS3DiscoveryProcessor(env.reader));
```
**After:**
```yaml
# app-config.yaml
catalog:
providers:
awsS3:
yourProviderId: # identifies your dataset / provider independent of config changes
bucketName: sample-bucket
prefix: prefix/ # optional
region: us-east-2 # optional, uses the default region otherwise
```
```ts
/* packages/backend/src/plugins/catalog.ts */
import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws';
const builder = await CatalogBuilder.create(env);
/** ... other processors and/or providers ... */
builder.addEntityProvider(
...AwsS3EntityProvider.fromConfig(env.config, {
logger: env.logger,
schedule: env.scheduler.createScheduledTaskRunner({
frequency: Duration.fromObject({ minutes: 30 }),
timeout: Duration.fromObject({ minutes: 3 }),
}),
}),
);
```
For simple setups, you can omit the provider ID at the config
which has the same effect as using `default` for it.
```yaml
# app-config.yaml
catalog:
providers:
awsS3:
# uses "default" as provider ID
bucketName: sample-bucket
prefix: prefix/ # optional
region: us-east-2 # optional, uses the default region otherwise
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-todo-backend': patch
---
Fix method to get source-location.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
The backend will no longer fail to start up when configured secrets do not match the configuration schema.
+61
View File
@@ -0,0 +1,61 @@
---
'@backstage/plugin-kubernetes-backend': minor
---
**BREAKING** Custom cluster suppliers need to cache their getClusters result
To allow custom `KubernetesClustersSupplier` instances to refresh the list of clusters
the `getClusters` method is now called whenever the list of clusters is needed.
Existing `KubernetesClustersSupplier` implementations will need to ensure that `getClusters`
can be called frequently and should return a cached result from `getClusters` instead.
For example, here's a simple example of a custom supplier in `packages/backend/src/plugins/kubernetes.ts`:
```diff
-import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
+import {
+ ClusterDetails,
+ KubernetesBuilder,
+ KubernetesClustersSupplier,
+} from '@backstage/plugin-kubernetes-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
+import { Duration } from 'luxon';
+
+export class CustomClustersSupplier implements KubernetesClustersSupplier {
+ constructor(private clusterDetails: ClusterDetails[] = []) {}
+
+ static create(refreshInterval: Duration) {
+ const clusterSupplier = new CustomClustersSupplier();
+ // setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts
+ runPeriodically(
+ () => clusterSupplier.refreshClusters(),
+ refreshInterval.toMillis(),
+ );
+ return clusterSupplier;
+ }
+
+ async refreshClusters(): Promise<void> {
+ this.clusterDetails = []; // fetch from somewhere
+ }
+
+ async getClusters(): Promise<ClusterDetails[]> {
+ return this.clusterDetails;
+ }
+}
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
- const { router } = await KubernetesBuilder.createBuilder({
+ const builder = await KubernetesBuilder.createBuilder({
logger: env.logger,
config: env.config,
- }).build();
+ });
+ builder.setClusterSupplier(
+ CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })),
+ );
+ const { router } = await builder.build();
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-aws': patch
---
Corrected title and URL to setup documentation in README
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/create-app': patch
---
Removed `@octokit/rest` and `@gitbeaker/node` from backend dependencies as these are unused in the default app.
To apply these changes to your existing app, remove the following lines from the `dependencies` section of `packages/backend/package.json`
```diff
"@backstage/plugin-techdocs-backend": "^1.0.0",
- "@gitbeaker/node": "^34.6.0",
- "@octokit/rest": "^18.5.3",
```
+25
View File
@@ -0,0 +1,25 @@
---
'@backstage/create-app': patch
---
Add type resolutions for `@types/react` and `types/react-dom`.
The reason for this is the usage of `"@types/react": "*"` as a dependency which is very common practice in react packages. This recently resolves to react 18 which introduces several breaking changes in both internal and external packages.
To apply these changes to your existing installation, add a resolutions block to your `package.json`
```json
"resolutions": {
"@types/react": "^17",
"@types/react-dom": "^17"
},
```
If your existing app depends on react 16, use this resolution block instead.
```json
"resolutions": {
"@types/react": "^16",
"@types/react-dom": "^16"
},
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Added `spec.profile.displayName` to search index for Group kinds
+84
View File
@@ -0,0 +1,84 @@
---
'@backstage/plugin-search-backend-node': minor
'@backstage/create-app': patch
---
**BREAKING**: `IndexBuilder.addCollator()` now requires a `schedule` parameter (replacing `defaultRefreshIntervalSeconds`) which is expected to be a `TaskRunner` that is configured with the desired search indexing schedule for the given collator.
`Scheduler.addToSchedule()` now takes a new parameter object (`ScheduleTaskParameters`) with two new options `id` and `scheduledRunner` in addition to the migrated `task` argument.
NOTE: The search backend plugin now creates a dedicated database for coordinating indexing tasks.
To make this change to an existing app, make the following changes to `packages/backend/src/plugins/search.ts`:
```diff
+import { Duration } from 'luxon';
/* ... */
+ const schedule = env.scheduler.createScheduledTaskRunner({
+ frequency: Duration.fromObject({ minutes: 10 }),
+ timeout: Duration.fromObject({ minutes: 15 }),
+ initialDelay: Duration.fromObject({ seconds: 3 }),
+ });
indexBuilder.addCollator({
- defaultRefreshIntervalSeconds: 600,
+ schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
indexBuilder.addCollator({
- defaultRefreshIntervalSeconds: 600,
+ schedule,
factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
const { scheduler } = await indexBuilder.build();
- setTimeout(() => scheduler.start(), 3000);
+ scheduler.start();
/* ... */
```
NOTE: For scenarios where the `lunr` search engine is used in a multi-node configuration, a non-distributed `TaskRunner` like the following should be implemented to ensure consistency across nodes (alternatively, you can configure
the search plugin to use a non-distributed DB such as [SQLite](https://backstage.io/docs/tutorials/configuring-plugin-databases#postgresql-and-sqlite-3)):
```diff
+import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks';
/* ... */
+ const schedule: TaskRunner = {
+ run: async (task: TaskInvocationDefinition) => {
+ const startRefresh = async () => {
+ while (!task.signal?.aborted) {
+ try {
+ await task.fn(task.signal);
+ } catch {
+ // ignore intentionally
+ }
+
+ await new Promise(resolve => setTimeout(resolve, 600 * 1000));
+ }
+ };
+ startRefresh();
+ },
+ };
indexBuilder.addCollator({
- defaultRefreshIntervalSeconds: 600,
+ schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
/* ... */
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Defensively encode URL parameters when fetching ELB keys
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-cicd-statistics-module-gitlab': minor
---
Created a module to extract the CI/CD statistics from a Gitlab repository.
Read the `README.md` in the `cicd-statistics-module-gitlab` plugin folder on how to set it up.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Updated the "unregister location" behavior in `UnregisterEntityDialog`. Removed unnecessary entity deletion requests that were sent after successfully deleting a location.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Fixed a bug were the `react-hot-loader` transform was being applied to backend development builds.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-jenkins-backend': patch
---
feature: provide access token to JenkinsInstanceConfig. It can be passed to other backend calls if authentication enabled. DefaultJenkinsInfoProvider sends always this token to catalog api if access token exists.
+21
View File
@@ -0,0 +1,21 @@
---
'@backstage/create-app': patch
---
Accept `PermissionEvaluator` instead of the deprecated `PermissionAuthorizer`.
Apply the following to `packages/backend/src/types.ts`:
```diff
- import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
+ import { PermissionEvaluator } from '@backstage/plugin-permission-common';
export type PluginEnvironment = {
...
discovery: PluginEndpointDiscovery;
tokenManager: TokenManager;
scheduler: PluginTaskScheduler;
- permissions: PermissionAuthorizer;
+ permissions: PermissionEvaluator;
};
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-react': minor
---
New search package to hold things the search plugin itself and other frontend plugins (e.g. techdocs, home) depend on.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-home': patch
---
Updated the dependency on `@backstage/config` to `^1.0.0`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-api-docs': patch
---
Add dedicated gRPC api definition widget
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-permission-react': patch
---
**BREAKING:** Make `IdentityPermissionApi#authorize` typing more strict, using `AuthorizePermissionRequest` and `AuthorizePermissionResponse`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/config-loader': minor
---
Added `ignoreSchemaErrors` to `schema.process`.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/plugin-search': patch
---
The following exports has been moved to `@backstage/plugin-search-react` and will be removed in the next release. import from `@backstage/plugin-search-react` instead.
- `SearchApi` interface.
- `searchApiRef`
- `SearchContext`
- `SearchContextProvider`
- `useSearch`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-gocd': patch
---
Add DORA metrics insights to GoCD builds page
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-insights': minor
---
Add checksId option to EntityTechInsightsScorecardContent component
+1
View File
@@ -39,6 +39,7 @@ Changesets
chanwit
Chanwit
ci
CI/CD
classname
cli
cloudbuild
+4 -1
View File
@@ -110,7 +110,7 @@ jobs:
continue-on-error: true
- name: prettier
run: yarn prettier:check '!ADOPTERS.md'
run: yarn prettier:check
- name: lock
run: yarn lock:check
@@ -137,6 +137,9 @@ jobs:
- name: verify doc links
run: node scripts/verify-links.js
- name: verify local dependency ranges
run: node scripts/verify-local-dependencies.js
- name: build changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
run: yarn backstage-cli repo build --all --since origin/master
+1
View File
@@ -5,6 +5,7 @@ on:
jobs:
label-issue:
runs-on: ubuntu-latest
if: github.repository == 'backstage/backstage'
steps:
- name: View context attributes
uses: actions/github-script@v6
+3
View File
@@ -8,3 +8,6 @@ api-report.md
plugins/scaffolder-backend/sample-templates
.vscode
dist-types
# reduce the barrier for adopters to add themselves
ADOPTERS.md
+5 -2
View File
@@ -26,7 +26,7 @@ _If you're using Backstage in your organization, please try to add your company
| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling |
| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. |
| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. |
| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. |
| [Peloton](https://www.onepeloton.com/) | [Matt Waldron](https://github.com/daftgopher) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. |
| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. |
| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teams engineering dependencies. |
| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. |
@@ -34,7 +34,7 @@ _If you're using Backstage in your organization, please try to add your company
| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. |
| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. |
| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. |
| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 |
| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 |
| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes |
| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). |
| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. |
@@ -110,3 +110,6 @@ _If you're using Backstage in your organization, please try to add your company
| [Beez Innovation Labs Pvt. Ltd](https://www.beezlabs.com/) | [Karthikeyan Venkatesan](https://github.com/karthikeyan23) | Developer portal with software catalog, scaffolding, tech docs, templates, and infra. |
| [Agorapulse](https://www.agorapulse.com/) | [@jvdrean](https://github.com/jvdrean) | Developer portal with software catalog, documentation, monitoring, runbooks, tech radar and more. |
| [Wistia](https://wistia.com/) | [@qrush](https://github.com/qrush), [@okize](https://github.com/okize) | Internal Developer Portal, service catalog, tech docs and more |
| [SIX](https://www.six-group.com/) | [@jbadeau](https://github.com/jbadeau), [@tomassatka](https://github.com/tomassatka) | Internal DevOps portal hosting our software and dataset catalog, as well as custom plugins for observability, service virtualization, deployments, incident managment and quality metrics. |
| [Raiffeisen Bank International](https://www.rbinternational.com/) | [Daniel Baumgartner](https://github.com/dabarbi) | From developers for developers: software catalog, techdocs and heavy use of scaffolder to drive reuse on engineering level forward. Part of inner source initiative. Multi national setup coming. |
| [Spread Group](https://www.spreadgroup.com/) | [Luna Stadler](https://github.com/heyLu), [Iván González](https://github.com/ivangonzalezacuna) | Internal Developer Portal, an overview of all running software, architecture documentation and more; replacing and unifying a variety of internal tools. |
@@ -0,0 +1,269 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="521px" height="601px" viewBox="-0.5 -0.5 521 601" content="&lt;mxfile&gt;&lt;diagram id=&quot;M4OCM2KiCGRnt6vHj1W_&quot; name=&quot;Page-1&quot;&gt;zVpNc6M4EP01HOMyAuP4GDvzkard2VTlsJujAMVoAogSENvz61ctJIwQzmQSYuODjRohxOvX3U/CjrfJ9t84LpK/WUxSB83jvePdOggtAld8g+HQGFw0XzaWLaexsh0ND/QXUca5stY0JqXRsWIsrWhhGiOW5ySqDBvmnO3Mbk8sNe9a4C2xDA8RTm3rvzSuEvVc/vxo/07oNlF3DpA6EeLoectZnavbOch7kp/mdIb1UKp/meCY7Tom74vjbThjVXOU7TckBWg1as11X0+cbafNSV695QLUXPCC05roGct5VQcNhXwaAv3njrfeJbQiDwWO4OxOOF/YkipLRcsVh2XF2TPZsJRxYclZLrqt7Umpeb4QXpF9x6Qm+Y2wjFT8ILrsTV4czObu6BttSjpu0Tas2LBtxz1CIg4UKsMIrSxASCyooprqCTsIkD2t/uscPwJws4Vq3e4VjrJxUI2TAJWs5pG6baDCAPMt6T0fzOhVFDlJcUVfTHIPoaIuvWdUzKRFH12b8KN5D9hmUuqqHrbtNN4Et6upcWTkDzbogr9wKJKOgT1O6TYXx5HAkQgCroFgVET1jTqR0TiGMdaclPQXDuV44IECpi4fZrF2FreDPlmdIG2bhdR4RigPkflqPgvQ0jUZ3bQ+6KirlTGobuoB2NNTST7sIe+dEXGMgsfOmZEjAo0dEW/NE+7CIu6jqGCTYK522SjUXQTB0mSZNwp3vaF4GJW5geWhuxL4kxDxnZMd3C+OaUVZ7qAAZ1Db8rCEn6IOUxqJDjf3d473VZxOBbbrkIujLRzRGZkB7/YFExjHoDk4y9rRRcF8BslhFdeEZWFd/r6wjlBDUa+GIruIIn+wio5QRq+nkjSWdtIILpUzljYhc3BIgiEuirTe0rxLHiEXWZ0Cue7AXEOv7CCjOCaSlie1m3sWigV9nWAxzA0GGBaMQDCthTpg3kuAqPzK7VDsh3DVwF6XkLblZRdG0+vB6frnxHP5zogdXfjqmm7U+YvFrHtt8eyfPIVb4aIQF8MKkIjl3zQKv84wn1L4x9GsGtDP1Kztcvvi5WeQzKuLkdle/3ckUSNm4AHqomAlqBorZ4q12TwkTdqEwhTCPJm4nrcVDAZk0PxZl5UdJzohl4MF7HL6qM2iZ9FHri1Oh8uXkJUiU0jgJKgdHC29MLH61W4jnKN+IX8y9Wtlh7xe7Zw/5JG9Tp3KBot22Tg7LGZZQaPUqqvArFXXn1Gr3iu8xq9VQ8S9mPBCtvCazAYLGlNn+QuTZB+UWeNu/q0sJzzoNWlPNIQDS6y6lDgomRAKnSB+fojF6+wnqAQs61gIr28a/TBNZeDPz6kM2un2eL/5neiaQmCcgvqP46L/Bsh77d3OieS9NLcYA/8zsrdneavRcR3d1oRIqXVdu9cog0WrOGWTYXPFCY6qxuUqRlrV9+eD5bBZNDVt6J9zr2hgJ/gmjrXYlmuaN+AYsSyDnWLwSljrlCcWS5zoUeTCqcmGw7luR0LwYf5CBT0yAvF0ac/4pmMWA5tO7fu/0R1jl5dX8Ljg++hFryQEAyVhpDfSonn8P0CTkY7/ufC+/A8=&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(255, 255, 255);">
<defs/>
<g>
<rect x="0" y="0" width="10" height="10" fill="rgb(255, 255, 255)" stroke="none" pointer-events="all"/>
<path d="M 250 160 L 343.63 160" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 348.88 160 L 341.88 163.5 L 343.63 160 L 341.88 156.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 151px; margin-left: 261px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
No
</div>
</div>
</div>
</foreignObject>
<text x="261" y="154" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
No
</text>
</switch>
</g>
<path d="M 130 210 L 130 243.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 130 248.88 L 126.5 241.88 L 130 243.63 L 133.5 241.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 221px; margin-left: 131px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
Yes
</div>
</div>
</div>
</foreignObject>
<text x="131" y="224" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
Yes
</text>
</switch>
</g>
<path d="M 130 110 L 250 160 L 130 210 L 10 160 Z" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 238px; height: 1px; padding-top: 160px; margin-left: 11px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Is the new addition public API?
<br/>
i.e. exported from the package
</div>
</div>
</div>
</foreignObject>
<text x="130" y="164" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Is the new addition public API?...
</text>
</switch>
</g>
<path d="M 130 70 L 130 103.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 130 108.88 L 126.5 101.88 L 130 103.63 L 133.5 101.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="50" y="10" width="160" height="60" rx="9" ry="9" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 40px; margin-left: 51px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
In what plugin package should I put my code?
</div>
</div>
</div>
</foreignObject>
<text x="130" y="44" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
In what plugin package sho...
</text>
</switch>
</g>
<rect x="350" y="130" width="160" height="60" rx="9" ry="9" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 160px; margin-left: 351px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Put it in the package
<br/>
that uses it
</div>
</div>
</div>
</foreignObject>
<text x="430" y="164" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Put it in the package...
</text>
</switch>
</g>
<path d="M 250 300 L 343.63 300" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 348.88 300 L 341.88 303.5 L 343.63 300 L 341.88 296.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 291px; margin-left: 291px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
Only app/backend
</div>
</div>
</div>
</foreignObject>
<text x="291" y="294" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
Only app/backend
</text>
</switch>
</g>
<path d="M 130 350 L 130 383.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 130 388.88 L 126.5 381.88 L 130 383.63 L 133.5 381.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 130 250 L 250 300 L 130 350 L 10 300 Z" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 238px; height: 1px; padding-top: 300px; margin-left: 11px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Is the export supposed
<br/>
to be used by other plugins or just app/backend packages?
</div>
</div>
</div>
</foreignObject>
<text x="130" y="304" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Is the export supposed...
</text>
</switch>
</g>
<rect x="350" y="270" width="160" height="60" rx="9" ry="9" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 300px; margin-left: 351px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Put it in the frontend or backend plugin package
</div>
</div>
</div>
</foreignObject>
<text x="430" y="304" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Put it in the frontend or...
</text>
</switch>
</g>
<path d="M 250 440 L 343.63 440" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 348.88 440 L 341.88 443.5 L 343.63 440 L 341.88 436.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 431px; margin-left: 261px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
No
</div>
</div>
</div>
</foreignObject>
<text x="261" y="434" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
No
</text>
</switch>
</g>
<path d="M 130 490 L 130 523.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 130 528.88 L 126.5 521.88 L 130 523.63 L 133.5 521.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 502px; margin-left: 130px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
Yes
</div>
</div>
</div>
</foreignObject>
<text x="130" y="505" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
Yes
</text>
</switch>
</g>
<path d="M 130 390 L 250 440 L 130 490 L 10 440 Z" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 238px; height: 1px; padding-top: 440px; margin-left: 11px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Should the export be
<br/>
usable by both Node.js and browser packages?
</div>
</div>
</div>
</foreignObject>
<text x="130" y="444" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Should the export be...
</text>
</switch>
</g>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 365px; margin-left: 128px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
Yes, used by other plugins
</div>
</div>
</div>
</foreignObject>
<text x="128" y="368" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
Yes, used by other plugins
</text>
</switch>
</g>
<rect x="350" y="410" width="160" height="60" rx="9" ry="9" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 440px; margin-left: 351px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Put frontend exports in &lt;plugin&gt;-react, and backend exports in &lt;plugin&gt;-node
</div>
</div>
</div>
</foreignObject>
<text x="430" y="444" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Put frontend exports in &lt;p...
</text>
</switch>
</g>
<rect x="30" y="530" width="200" height="60" rx="9" ry="9" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 198px; height: 1px; padding-top: 560px; margin-left: 31px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Add it to &lt;plugin&gt;-common, but be sure to support both Node.js and web environments
</div>
</div>
</div>
</foreignObject>
<text x="130" y="564" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Add it to &lt;plugin&gt;-common, but be...
</text>
</switch>
</g>
<rect x="510" y="590" width="10" height="10" fill="rgb(255, 255, 255)" stroke="none" pointer-events="all"/>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Viewer does not support full SVG 1.1
</text>
</a>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 75 KiB

+10
View File
@@ -57,6 +57,10 @@ This is an array used to determine where to retrieve cluster configuration from.
Valid cluster locator methods are:
- [`config`](#config)
- [`gke`](#gke)
- [custom `KubernetesClustersSupplier`](#custom-kubernetesclusterssupplier)
#### `config`
This cluster locator method will read cluster information from your app-config
@@ -261,6 +265,12 @@ Kubernetes plugin.
Defaults to `false`.
#### Custom `KubernetesClustersSupplier`
If the configuration-based cluster locators do not work for your use-case,
it is also possible to implement a
[custom `KubernetesClustersSupplier`](installation.md#custom-cluster-discovery).
### `customResources` (optional)
Configures which [custom resources][3] to look for when returning an entity's
+57
View File
@@ -90,6 +90,63 @@ async function main() {
That's it! The Kubernetes frontend and backend have now been added to your
Backstage app.
### Custom cluster discovery
If either existing
[cluster locators](https://backstage.io/docs/features/kubernetes/configuration#clusterlocatormethods)
don't work for your use-case, it is possible to implement a custom
[KubernetesClustersSupplier](https://backstage.io/docs/reference/plugin-kubernetes-backend.kubernetesclusterssupplier).
Change the following in `packages/backend/src/plugin/kubernetes.ts`:
```diff
-import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
+import {
+ ClusterDetails,
+ KubernetesBuilder,
+ KubernetesClustersSupplier,
+} from '@backstage/plugin-kubernetes-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
+import { Duration } from 'luxon';
+
+export class CustomClustersSupplier implements KubernetesClustersSupplier {
+ constructor(private clusterDetails: ClusterDetails[] = []) {}
+
+ static create(refreshInterval: Duration) {
+ const clusterSupplier = new CustomClustersSupplier();
+ // setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts
+ runPeriodically(
+ () => clusterSupplier.refreshClusters(),
+ refreshInterval.toMillis(),
+ );
+ return clusterSupplier;
+ }
+
+ async refreshClusters(): Promise<void> {
+ this.clusterDetails = []; // fetch from somewhere
+ }
+
+ async getClusters(): Promise<ClusterDetails[]> {
+ return this.clusterDetails;
+ }
+}
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
- const { router } = await KubernetesBuilder.createBuilder({
+ const builder = await KubernetesBuilder.createBuilder({
logger: env.logger,
config: env.config,
- }).build();
+ });
+ builder.setClusterSupplier(
+ CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })),
+ );
+ const { router } = await builder.build();
```
## Running Backstage locally
Start the frontend and the backend app by
+3 -1
View File
@@ -84,7 +84,9 @@ index-time.
There are many ways a search index could be built and maintained, but Backstage
Search chooses to completely rebuild indices on a schedule. Different collators
can be configured to refresh at different intervals, depending on how often the
source information is updated.
source information is updated. When search indexing is distributed among multiple
backend nodes, coordination to prevent clashes is typically handled by a
distributed `TaskRunner`.
### The Search Page
+72 -10
View File
@@ -149,6 +149,7 @@ import {
import { PluginEnvironment } from '../types';
import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend';
import { Router } from 'express';
import { Duration } from 'luxon';
export default async function createPlugin(
env: PluginEnvironment,
@@ -161,9 +162,15 @@ export default async function createPlugin(
searchEngine,
});
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
frequency: Duration.fromObject({ minutes: 10 }),
timeout: Duration.fromObject({ minutes: 15 }),
initialDelay: Duration.fromObject({ seconds: 3 }),
});
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
collator: new DefaultCatalogCollator({
schedule: every10MinutesSchedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
@@ -287,32 +294,87 @@ which are responsible for providing documents
number of collators with the `IndexBuilder` like this:
```typescript
import { Duration } from 'luxon';
const indexBuilder = new IndexBuilder({ logger: env.logger, searchEngine });
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
frequency: Duration.fromObject({ minutes: 10 }),
timeout: Duration.fromObject({ minutes: 15 }),
initialDelay: Duration.fromObject({ seconds: 3 }),
});
const everyHourSchedule = env.scheduler.createScheduledTaskRunner({
frequency: Duration.fromObject({ hours: 1 }),
timeout: Duration.fromObject({ minutes: 90 }),
initialDelay: Duration.fromObject({ seconds: 3 }),
});
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
collator: new DefaultCatalogCollator({
schedule: every10MinutesSchedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 3600,
collator: new MyCustomCollator(),
schedule: everyHourSchedule,
factory: new MyCustomCollatorFactory(),
});
```
Backstage Search builds and maintains its index
[on a schedule](./concepts.md#the-scheduler). You can change how often the
indexes are rebuilt for a given type of document. You may want to do this if
your documents are updated more or less frequently. You can do so by modifying
its `defaultRefreshIntervalSeconds` value, like this:
your documents are updated more or less frequently. You can do so by configuring
a scheduled `TaskRunner` to pass into the `schedule` value, like this:
```typescript {3}
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
frequency: Duration.fromObject({ minutes: 10 }),
timeout: Duration.fromObject({ minutes: 15 }),
initialDelay: Duration.fromObject({ seconds: 3 }),
});
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
collator: new DefaultCatalogCollator({
schedule: every10MinutesSchedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
```
Note: if you are using the in-memory Lunr search engine, you probably want to
implement a non-distributed `TaskRunner` like the following to ensure consistency
if you're running multiple search backend nodes (alternatively, you can configure
the search plugin to use a non-distributed database such as
[SQLite](../../tutorials/configuring-plugin-databases.md#postgresql-and-sqlite-3)):
```typescript
import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks';
const schedule: TaskRunner = {
run: async (task: TaskInvocationDefinition) => {
const startRefresh = async () => {
while (!task.signal?.aborted) {
try {
await task.fn(task.signal);
} catch {
// ignore intentionally
}
await new Promise(resolve => setTimeout(resolve, 600 * 1000));
}
};
startRefresh();
},
};
indexBuilder.addCollator({
schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
+13 -18
View File
@@ -10,29 +10,20 @@ description: TechDocs is Spotifys homegrown docs-like-code solution built dir
<!-- Intro, backstory, etc.: -->
TechDocs is Spotifys homegrown docs-like-code solution built directly into
Backstage. This means engineers write their documentation in Markdown files
which live together with their code.
TechDocs is Spotifys homegrown docs-like-code solution built directly into Backstage. Engineers write their documentation in Markdown files which live together with their code - and with little configuration get a nice-looking doc site in Backstage.
Today, it is one of the core products in Spotifys developer experience offering
with 2,400+ documentation sites and 1,000+ engineers using it daily. Read more
about TechDocs and the philosophy in its
Today, it is one of the core products in Spotifys developer experience offering with 5000+ documentation sites and around 10000 average daily hits. Read more about TechDocs in its
[announcement blog post](https://backstage.io/blog/2020/09/08/announcing-tech-docs).
🎉
## Features
- Deploy TechDocs no matter how your software environment is set up.
- Discover your Service's technical documentation from the Service's page in
Backstage Catalog.
- Discover your Service's technical documentation from the Service's page in Backstage Catalog.
- Create documentation-only sites for any purpose by just writing Markdown.
- Explore and take advantage of the large ecosystem of
[MkDocs plugins](https://www.mkdocs.org/user-guide/plugins/) to create a rich
reading experience.
[MkDocs plugins](https://www.mkdocs.org/user-guide/plugins/) to create a rich reading experience.
- Search for and find docs.
- Highlight text and raise an Issue to create feedback loop to drive quality
documentation (future).
- Contribute to and deploy from a marketplace of TechDocs widgets (future).
## Platforms supported
@@ -83,17 +74,21 @@ TechDocs packages:
- '@backstage/plugin-techdocs-node'
- '@techdocs/cli'
was promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy).
TechDocs promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy).
### **Future work 🔮**
Some of the following items are coming soon and some are potential ideas.
- [TechDocs Addon Framework](https://github.com/backstage/backstage/issues/9636)
- Contribute to and deploy from a marketplace of TechDocs Addons
- Addon: Highlight text and raise an Issue to create a feedback loop to drive up documentation quality
- Addon: MDX (allows you to use JSX in your Markdown content)
- Better integration with
[Scaffolder V2](https://github.com/backstage/backstage/issues/2771) (e.g. easy
to choose and plug documentation template with Software Templates).
[Scaffolder V2](https://github.com/backstage/backstage/issues/2771) (e.g. easy to choose and plug documentation template with Software Templates)
- Static site generator agnostic
- Possible to configure several aspects about TechDocs (e.g. URL, homepage,
theme).
- [TechDocs Addon Framework](https://github.com/backstage/backstage/issues/9636)
theme)
## Tech stack
+68 -13
View File
@@ -6,32 +6,55 @@ sidebar_label: Discovery
description: Automatically discovering catalog entities from an AWS S3 Bucket
---
The AWS S3 integration has a special discovery processor for discovering catalog
The AWS S3 integration has a special entity provider for discovering catalog
entities located in an S3 Bucket. If you have a bucket that contains multiple
catalog-info files and want to automatically discover them, you can use this
processor. The processor will crawl your S3 bucket and register entities
catalog files, and you want to automatically discover them, you can use this
provider. The provider will crawl your S3 bucket and register entities
matching the configured path. This can be useful as an alternative to static
locations or manually adding things to the catalog.
To use the discovery processor, you'll need an AWS S3 integration
[set up](locations.md) with an `AWS_ACCESS_KEY`, `AWS_SECRET_ACCESS_KEY`, and
optionally a `roleArn`. Then you can add a location target to the catalog
configuration:
To use the entity provider, you'll need an AWS S3 integration
[set up](locations.md) with `accessKeyId` and `secretAccessKey`, and/or
a `roleArn` or none of these (e.g., profile- or instance-based credentials).
At production deployments, you likely manage these with the permissions attached
to your instance.
At your configuration, you add a provider config per bucket:
```yaml
# app-config.yaml
catalog:
locations:
- type: s3-discovery
target: https://sample-bucket.s3.us-east-2.amazonaws.com/
providers:
awsS3:
yourProviderId: # identifies your dataset / provider independent of config changes
bucketName: sample-bucket
prefix: prefix/ # optional
region: us-east-2 # optional, uses the default region otherwise
```
Note the `s3-discovery` type, as this is not a regular `url` processor.
For simple setups, you can omit the provider ID at the config
which has the same effect as using `default` for it.
As this processor is not one of the default providers, you will first need to install the AWS catalog plugin:
```yaml
# app-config.yaml
catalog:
providers:
awsS3:
# uses "default" as provider ID
bucketName: sample-bucket
prefix: prefix/ # optional
region: us-east-2 # optional, uses the default region otherwise
```
As this provider is not one of the default providers, you will first need to install
the AWS catalog plugin:
```bash
# From the Backstage root directory
yarn install --cwd packages/backend @backstage/plugin-catalog-backend-module-aws
yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-aws
```
Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`:
@@ -39,6 +62,38 @@ Once you've done that, you'll also need to add the segment below to `packages/ba
```ts
/* packages/backend/src/plugins/catalog.ts */
import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws';
const builder = await CatalogBuilder.create(env);
/** ... other processors and/or providers ... */
builder.addEntityProvider(
...AwsS3EntityProvider.fromConfig(env.config, {
logger: env.logger,
schedule: env.scheduler.createScheduledTaskRunner({
frequency: Duration.fromObject({ minutes: 30 }),
timeout: Duration.fromObject({ minutes: 3 }),
}),
}),
);
```
## Alternative Processor
As alternative to the entity provider `AwsS3EntityProvider`
you can still use the `AwsS3DiscoveryProcessor`.
```yaml
# app-config.yaml
catalog:
locations:
- type: s3-discovery
target: https://sample-bucket.s3.us-east-2.amazonaws.com/prefix/
```
```ts
/* packages/backend/src/plugins/catalog.ts */
import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-aws';
const builder = await CatalogBuilder.create(env);
+43
View File
@@ -0,0 +1,43 @@
---
id: locations
title: Gerrit Locations
sidebar_label: Locations
description: Integrating source code stored in Gerrit into the Backstage catalog
---
The Gerrit integration supports loading catalog entities from Gerrit hosted gits. Entities can
be added to [static catalog configuration](../../features/software-catalog/configuration.md),
or registered with the
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
plugin.
## Configuration
To use this integration, add at least one Gerrit configuration to your root `app-config.yaml`:
```yaml
integrations:
gerrit:
- host: gerrit.company.com
apiBaseUrl: https://gerrit.company.com/gerrit
gitilesBaseUrl: https://gerrit.company.com/gitiles
username: ${GERRIT_USERNAME}
password: ${GERRIT_PASSWORD}
```
Directly under the `gerrit` key is a list of provider configurations, where
you can list the Gerrit instances you want to fetch data from. Each entry is
a structure with up to four elements:
- `host`: The host of the Gerrit instance, e.g. `gerrit.company.com`.
- `apiBaseUrl` (optional): Needed if the Gerrit instance is not reachable at
the base of the `host` option (e.g. `https://gerrit.company.com`) set the
address here. This is the address that you would open in a browser.
- `gitilesBaseUrl` (optional): This is needed for creating a valid user-friendly url
that can be used for browsing the content of the provider. If not set a default
value will be created in the same way as the "baseUrl" option. There is no
requirement to have Gitiles for the Backstage Gerrit integration but without it
some links in the Backstage UI will be broken.
- `username` (optional): The Gerrit username to use in API requests. If
neither a username nor password are supplied, anonymous access will be used.
- `password` (optional): The password or http token for the Gerrit user.
+3 -2
View File
@@ -16,11 +16,12 @@ catalog.
You will have to add the processors in the catalog initialization code of your
backend. They are not installed by default, therefore you have to add a
dependency to `@backstage/plugin-catalog-backend-module-github` to your backend
package.
dependency on `@backstage/plugin-catalog-backend-module-github` to your backend
package, plus `@backstage/integration` for the basic credentials management:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/integration
yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-github
```
+12
View File
@@ -256,6 +256,18 @@ The Backstage CLI is in a category of its own and is depended on by virtually
all other packages. It's not a library in itself though, and must always be a
development dependency only.
### Deciding where you place your code
It can sometimes be difficult to decide where to place your plugin code. For example
should it go directly in the `-backend` plugin package or in the `-node` package?
As a rule of thumb you should try to keep the exposure of your code as low
as possible. If it doesn't need to be public API, it's best to avoid. If you don't
need it to be used by other plugins, then keep it directly in the plugin packages.
Below is a chart to help you decide where to place your code.
![Package decision](../assets/architecture-overview/package-decision.drawio.svg)
## Databases
As we have seen, both the `lighthouse-audit-service` and `catalog-backend`
+5
View File
@@ -153,6 +153,11 @@
"label": "Datadog",
"ids": ["integrations/datadog-rum/installation"]
},
{
"type": "subcategory",
"label": "Gerrit",
"ids": ["integrations/gerrit/locations"]
},
{
"type": "subcategory",
"label": "GitHub",
+2
View File
@@ -97,6 +97,8 @@ nav:
- Discovery: 'integrations/bitbucket/discovery.md'
- Datadog:
- Installation: 'integrations/datadog-rum/installation.md'
- Gerrit:
- Locations: 'integrations/gerrit/locations.md'
- GitHub:
- Locations: 'integrations/github/locations.md'
- Discovery: 'integrations/github/discovery.md'
+7 -5
View File
@@ -45,15 +45,17 @@
]
},
"resolutions": {
"**/@graphql-codegen/cli/**/ws": "^7.4.6"
"**/@graphql-codegen/cli/**/ws": "^7.4.6",
"@types/react": "^17",
"@types/react-dom": "^17"
},
"version": "1.1.0-next.2",
"dependencies": {
"@manypkg/get-packages": "^1.1.3",
"@microsoft/api-documenter": "^7.17.0",
"@microsoft/api-extractor": "^7.19.4",
"@microsoft/api-extractor-model": "^7.16.0",
"@microsoft/tsdoc": "^0.13.2"
"@microsoft/api-documenter": "^7.17.5",
"@microsoft/api-extractor": "^7.21.2",
"@microsoft/api-extractor-model": "^7.16.1",
"@microsoft/tsdoc": "^0.14.1"
},
"devDependencies": {
"@changesets/cli": "^2.14.0",
+3
View File
@@ -10,6 +10,7 @@
"@backstage/app-defaults": "^1.0.1-next.1",
"@backstage/catalog-model": "^1.0.1-next.0",
"@backstage/cli": "^0.17.0-next.1",
"@backstage/config": "^1.0.0",
"@backstage/core-app-api": "^1.0.1-next.0",
"@backstage/core-components": "^0.9.3-next.0",
"@backstage/core-plugin-api": "^1.0.0",
@@ -47,9 +48,11 @@
"@backstage/plugin-rollbar": "^0.4.4-next.0",
"@backstage/plugin-scaffolder": "^1.0.1-next.1",
"@backstage/plugin-search": "^0.7.5-next.0",
"@backstage/plugin-search-react": "^0.0.0",
"@backstage/plugin-search-common": "^0.3.3-next.1",
"@backstage/plugin-sentry": "^0.3.42-next.0",
"@backstage/plugin-shortcuts": "^0.2.5-next.0",
"@backstage/plugin-stack-overflow": "^0.1.0-next.0",
"@backstage/plugin-tech-radar": "^0.5.11-next.1",
"@backstage/plugin-techdocs": "^1.0.1-next.1",
"@backstage/plugin-todo": "^0.2.6-next.0",
@@ -14,14 +14,14 @@
* limitations under the License.
*/
import { TemplateBackstageLogo } from './TemplateBackstageLogo';
import { TemplateBackstageLogoIcon } from './TemplateBackstageLogoIcon';
import {
HomePageToolkit,
HomePageCompanyLogo,
HomePageStarredEntities,
} from '../plugin';
import { wrapInTestApp, TestApiProvider} from '@backstage/test-utils';
TemplateBackstageLogo,
TemplateBackstageLogoIcon
} from '@backstage/plugin-home';
import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
import { Content, Page, InfoCard } from '@backstage/core-components';
import {
starredEntitiesApiRef,
@@ -32,10 +32,9 @@ import { configApiRef } from '@backstage/core-plugin-api';
import { ConfigReader } from '@backstage/config';
import {
HomePageSearchBar,
SearchContextProvider,
searchApiRef,
searchPlugin,
} from '@backstage/plugin-search';
import { searchApiRef, SearchContextProvider } from '@backstage/plugin-search-react';
import { HomePageStackOverflowQuestions } from '@backstage/plugin-stack-overflow';
import { Grid, makeStyles } from '@material-ui/core';
import React, { ComponentType } from 'react';
@@ -54,10 +53,7 @@ export default {
<>
<TestApiProvider
apis={[
[
starredEntitiesApiRef,
starredEntitiesApi,
],
[starredEntitiesApiRef, starredEntitiesApi],
[searchApiRef, { query: () => Promise.resolve({ results: [] }) }],
[
configApiRef,
+1 -1
View File
@@ -41,7 +41,7 @@ const updateRedactionList = (
) => {
const secretAppConfigs = schema.process(configs, {
visibility: ['secret'],
withDeprecatedKeys: true,
ignoreSchemaErrors: true,
});
const secretConfig = ConfigReader.fromConfigs(secretAppConfigs);
const values = new Set<string>();
+3 -1
View File
@@ -68,6 +68,7 @@
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-prom-bundle": "^6.3.6",
"luxon": "^2.0.2",
"pg": "^8.3.0",
"pg-connection-string": "^2.3.0",
"prom-client": "^14.0.1",
@@ -77,7 +78,8 @@
"@backstage/cli": "^0.17.0-next.1",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5"
"@types/express-serve-static-core": "^4.17.5",
"@types/luxon": "^2.0.4"
},
"files": [
"dist"
+12 -5
View File
@@ -26,6 +26,7 @@ import {
} from '@backstage/plugin-search-backend-node';
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
import { Router } from 'express';
import { Duration } from 'luxon';
import { PluginEnvironment } from '../types';
async function createSearchEngine(
@@ -55,10 +56,18 @@ export default async function createPlugin(
searchEngine,
});
const schedule = env.scheduler.createScheduledTaskRunner({
frequency: Duration.fromObject({ minutes: 10 }),
timeout: Duration.fromObject({ minutes: 15 }),
// A 3 second delay gives the backend server a chance to initialize before
// any collators are executed, which may attempt requests against the API.
initialDelay: Duration.fromObject({ seconds: 3 }),
});
// Collators are responsible for gathering documents known to plugins. This
// particular collator gathers entities from the software catalog.
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
@@ -66,7 +75,7 @@ export default async function createPlugin(
});
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
schedule,
factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
logger: env.logger,
@@ -77,10 +86,8 @@ export default async function createPlugin(
// The scheduler controls when documents are gathered from collators and sent
// to the search engine for indexing.
const { scheduler } = await indexBuilder.build();
scheduler.start();
// A 3 second delay gives the backend server a chance to initialize before
// any collators are executed, which may attempt requests against the API.
setTimeout(() => scheduler.start(), 3000);
useHotCleanup(module, () => scheduler.stop());
return await createRouter({
+5 -2
View File
@@ -23,8 +23,11 @@ import {
TokenManager,
UrlReader,
} from '@backstage/backend-common';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import {
PermissionAuthorizer,
PermissionEvaluator,
} from '@backstage/plugin-permission-common';
export type PluginEnvironment = {
logger: Logger;
@@ -34,6 +37,6 @@ export type PluginEnvironment = {
reader: UrlReader;
discovery: PluginEndpointDiscovery;
tokenManager: TokenManager;
permissions: ServerPermissionClient;
permissions: PermissionEvaluator | PermissionAuthorizer;
scheduler: PluginTaskScheduler;
};
+2 -2
View File
@@ -93,7 +93,7 @@
"mini-css-extract-plugin": "^2.4.2",
"minimatch": "5.0.1",
"node-libs-browser": "^2.2.1",
"npm-packlist": "^3.0.0",
"npm-packlist": "^5.0.0",
"ora": "^5.3.0",
"postcss": "^8.1.0",
"process": "^0.11.10",
@@ -153,7 +153,7 @@
"ts-node": "^10.0.0"
},
"peerDependencies": {
"@microsoft/api-extractor": "^7.19.2"
"@microsoft/api-extractor": "^7.21.2"
},
"peerDependenciesMeta": {
"@microsoft/api-extractor": {
+1 -1
View File
@@ -226,7 +226,7 @@ export async function createBackendConfig(
// See frontend config
const externalPkgs = packages.filter(p => !isChildPath(paths.root, p.dir));
const { loaders } = transforms(options);
const { loaders } = transforms({ ...options, isBackend: true });
const runScriptNodeArgs = new Array<string>();
if (options.inspectEnabled) {
+3 -2
View File
@@ -25,12 +25,13 @@ type Transforms = {
type TransformOptions = {
isDev: boolean;
isBackend?: boolean;
};
export const transforms = (options: TransformOptions): Transforms => {
const { isDev } = options;
const { isDev, isBackend } = options;
const extraTransforms = isDev ? ['react-hot-loader'] : [];
const extraTransforms = isDev && !isBackend ? ['react-hot-loader'] : [];
// This ensures that styles inserted from the style-loader and any
// async style chunks are always given lower priority than JSS styles.
+1
View File
@@ -19,6 +19,7 @@ export type ConfigSchema = {
// @public
export type ConfigSchemaProcessingOptions = {
visibility?: ConfigVisibility[];
ignoreSchemaErrors?: boolean;
valueTransform?: TransformFunc<any>;
withFilteredKeys?: boolean;
withDeprecatedKeys?: boolean;
@@ -275,5 +275,12 @@ describe('loadConfigSchema', () => {
).toThrow(
"Config must have required property 'x a' { missingProperty=x a } at /other",
);
expect(
schema.process([{ data: { other: {} }, context: 'test' }], {
visibility: ['frontend'],
ignoreSchemaErrors: true,
}),
).toEqual([{ data: {}, context: 'test' }]);
});
});
+17 -9
View File
@@ -82,18 +82,26 @@ export async function loadConfigSchema(
return {
process(
configs: AppConfig[],
{ visibility, valueTransform, withFilteredKeys, withDeprecatedKeys } = {},
{
visibility,
valueTransform,
withFilteredKeys,
withDeprecatedKeys,
ignoreSchemaErrors,
} = {},
): AppConfig[] {
const result = validate(configs);
const visibleErrors = filterErrorsByVisibility(
result.errors,
visibility,
result.visibilityByDataPath,
result.visibilityBySchemaPath,
);
if (visibleErrors.length > 0) {
throw errorsToError(visibleErrors);
if (!ignoreSchemaErrors) {
const visibleErrors = filterErrorsByVisibility(
result.errors,
visibility,
result.visibilityByDataPath,
result.visibilityBySchemaPath,
);
if (visibleErrors.length > 0) {
throw errorsToError(visibleErrors);
}
}
let processedConfigs = configs;
@@ -117,6 +117,11 @@ export type ConfigSchemaProcessingOptions = {
*/
visibility?: ConfigVisibility[];
/**
* When set to `true`, any schema errors in the provided configuration will be ignored.
*/
ignoreSchemaErrors?: boolean;
/**
* A transform function that can be used to transform primitive configuration values
* during validation. The value returned from the transform function will be used
@@ -37,6 +37,10 @@
"prettier": "^2.3.2",
"typescript": "~4.5.4"
},
"resolutions": {
"@types/react": "^17",
"@types/react-dom": "^17"
},
"prettier": "@spotify/prettier-config",
"lint-staged": {
"*.{js,jsx,ts,tsx,mjs,cjs}": [
@@ -35,11 +35,10 @@
{{/if}}
"@backstage/plugin-search-backend-node": "^{{version '@backstage/plugin-search-backend-node'}}",
"@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}",
"@gitbeaker/node": "^34.6.0",
"@octokit/rest": "^18.5.3",
"dockerode": "^3.3.1",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"luxon": "^2.0.2",
{{#if dbTypePG}}
"pg": "^8.3.0",
{{/if}}
@@ -52,7 +51,8 @@
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5"
"@types/express-serve-static-core": "^4.17.5",
"@types/luxon": "^2.0.4"
},
"files": [
"dist"
@@ -11,6 +11,7 @@ import { PluginEnvironment } from '../types';
import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend';
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
import { Router } from 'express';
import { Duration } from 'luxon';
export default async function createPlugin(
env: PluginEnvironment,
@@ -31,10 +32,18 @@ export default async function createPlugin(
searchEngine,
});
const schedule = env.scheduler.createScheduledTaskRunner({
frequency: Duration.fromObject({ minutes: 10 }),
timeout: Duration.fromObject({ minutes: 15 }),
// A 3 second delay gives the backend server a chance to initialize before
// any collators are executed, which may attempt requests against the API.
initialDelay: Duration.fromObject({ seconds: 3 }),
});
// Collators are responsible for gathering documents known to plugins. This
// collator gathers entities from the software catalog.
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
@@ -43,7 +52,7 @@ export default async function createPlugin(
// collator gathers entities from techdocs.
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
schedule,
factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
logger: env.logger,
@@ -54,10 +63,8 @@ export default async function createPlugin(
// The scheduler controls when documents are gathered from collators and sent
// to the search engine for indexing.
const { scheduler } = await indexBuilder.build();
scheduler.start();
// A 3 second delay gives the backend server a chance to initialize before
// any collators are executed, which may attempt requests against the API.
setTimeout(() => scheduler.start(), 3000);
useHotCleanup(module, () => scheduler.stop());
return await createRouter({
@@ -8,7 +8,7 @@ import {
UrlReader,
} from '@backstage/backend-common';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
export type PluginEnvironment = {
logger: Logger;
@@ -19,5 +19,5 @@ export type PluginEnvironment = {
discovery: PluginEndpointDiscovery;
tokenManager: TokenManager;
scheduler: PluginTaskScheduler;
permissions: PermissionAuthorizer;
permissions: PermissionEvaluator;
};
@@ -17,6 +17,7 @@ import React from 'react';
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget';
import { GraphQlDefinitionWidget } from '../GraphQlDefinitionWidget';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
import { GrpcApiDefinitionWidget } from '../GrpcApiDefinitionWidget';
export type ApiDefinitionWidget = {
type: string;
@@ -51,5 +52,12 @@ export function defaultDefinitionWidgets(): ApiDefinitionWidget[] {
<GraphQlDefinitionWidget definition={definition} />
),
},
{
type: 'grpc',
title: 'gRPC',
component: definition => (
<GrpcApiDefinitionWidget definition={definition} />
),
},
];
}
@@ -0,0 +1,32 @@
/*
* Copyright 2020 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 { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { GrpcApiDefinitionWidget } from './GrpcApiDefinitionWidget';
describe('<GrpcApiDefinitionWidget />', () => {
it('renders plain text', async () => {
const { getAllByText } = await renderInTestApp(
<GrpcApiDefinitionWidget definition="Hello World" />,
);
expect(
getAllByText((_text, element) => element?.textContent === 'Hello World')
.length,
).toBeGreaterThan(0);
});
});
@@ -0,0 +1,38 @@
/*
* Copyright 2020 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 React from 'react';
import { CodeSnippet } from '@backstage/core-components';
import { useTheme } from '@material-ui/core/styles';
import { BackstageTheme } from '@backstage/theme';
export type GrpcApiDefinitionWidgetProps = {
definition: string;
};
export const GrpcApiDefinitionWidget = (
props: GrpcApiDefinitionWidgetProps,
) => {
const theme = useTheme<BackstageTheme>();
return (
<CodeSnippet
customStyle={{ backgroundColor: theme.palette.background.default }}
text={props.definition}
language="protobuf"
showCopyCodeButton
/>
);
};
@@ -0,0 +1,18 @@
/*
* Copyright 2020 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.
*/
export { GrpcApiDefinitionWidget } from './GrpcApiDefinitionWidget';
export type { GrpcApiDefinitionWidgetProps } from './GrpcApiDefinitionWidget';
@@ -211,8 +211,10 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
if (optionalCacheKey) {
return crypto.createPublicKey(optionalCacheKey);
}
const keyText: string = await fetch(
`https://public-keys.auth.elb.${this.region}.amazonaws.com/${keyId}`,
const keyText = await fetch(
`https://public-keys.auth.elb.${encodeURIComponent(
this.region,
)}.amazonaws.com/${encodeURIComponent(keyId)}`,
).then(response => response.text());
const keyValue = crypto.createPublicKey(keyText);
this.keyCache.set(keyId, keyValue.export({ format: 'pem', type: 'spki' }));
+2 -2
View File
@@ -1,4 +1,4 @@
# Catalog Backend Module for LDAP
# Catalog Backend Module for AWS
This is an extension module to the plugin-catalog-backend plugin, providing an
`AwsOrganizationCloudAccountProcessor` that can be used to ingest cloud accounts
@@ -6,5 +6,5 @@ as `Resource` kind entities.
## Getting started
See [Backstage documentation](https://backstage.io/docs/integrations/ldap/org) for details on how to install
See [Backstage documentation](https://backstage.io/docs/integrations/aws-s3/discovery) for details on how to install
and configure the plugin.
@@ -7,8 +7,11 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-backend';
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
import { CatalogProcessorParser } from '@backstage/plugin-catalog-backend';
import { Config } from '@backstage/config';
import { EntityProvider } from '@backstage/plugin-catalog-backend';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
import { LocationSpec } from '@backstage/plugin-catalog-backend';
import { Logger } from 'winston';
import { TaskRunner } from '@backstage/backend-tasks';
import { UrlReader } from '@backstage/backend-common';
// @public
@@ -43,4 +46,22 @@ export class AwsS3DiscoveryProcessor implements CatalogProcessor {
parser: CatalogProcessorParser,
): Promise<boolean>;
}
// @public
export class AwsS3EntityProvider implements EntityProvider {
// (undocumented)
connect(connection: EntityProviderConnection): Promise<void>;
// (undocumented)
static fromConfig(
configRoot: Config,
options: {
logger: Logger;
schedule: TaskRunner;
},
): AwsS3EntityProvider[];
// (undocumented)
getProviderName(): string;
// (undocumented)
refresh(logger: Logger): Promise<void>;
}
```
+32
View File
@@ -14,6 +14,27 @@
* limitations under the License.
*/
interface AwsS3Config {
/**
* (Required) AWS S3 Bucket Name
* @visibility backend
*/
bucketName: string;
/**
* (Optional) AWS S3 Object key prefix
* If not set, all keys will be accepted, no filtering will be applied.
* @visibility backend
*/
prefix?: string;
/**
* (Optional) AWS Region.
* If not set, AWS_REGION environment variable or aws config file will be used.
* @see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html
* @visibility backend
*/
region?: string;
}
export interface Config {
catalog?: {
/**
@@ -32,5 +53,16 @@ export interface Config {
};
};
};
/**
* List of provider-specific options and attributes
*/
providers?: {
/**
* AwsS3EntityProvider configuration
*
* Uses "default" as default id for the single config variant.
*/
awsS3?: AwsS3Config | Record<string, AwsS3Config>;
};
};
}
@@ -34,14 +34,17 @@
},
"dependencies": {
"@backstage/backend-common": "^0.13.2-next.1",
"@backstage/backend-tasks": "^0.3.0-next.1",
"@backstage/catalog-model": "^1.0.1-next.0",
"@backstage/config": "^1.0.0",
"@backstage/errors": "^1.0.0",
"@backstage/integration": "^1.1.0-next.1",
"@backstage/plugin-catalog-backend": "^1.1.0-next.1",
"@backstage/types": "^1.0.0",
"aws-sdk": "^2.840.0",
"lodash": "^4.17.21",
"p-limit": "^3.0.2",
"uuid": "^8.0.0",
"winston": "^3.2.1"
},
"devDependencies": {
@@ -0,0 +1,61 @@
/*
* 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 aws, { Credentials } from 'aws-sdk';
import { CredentialsOptions } from 'aws-sdk/lib/credentials';
export class AwsCredentials {
/**
* If accessKeyId and secretAccessKey are missing, the DefaultAWSCredentialsProviderChain will be used:
* https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
*/
static create(
config: {
accessKeyId?: string;
secretAccessKey?: string;
roleArn?: string;
},
roleSessionName: string,
): Credentials | CredentialsOptions | undefined {
if (!config) {
return undefined;
}
const accessKeyId = config.accessKeyId;
const secretAccessKey = config.secretAccessKey;
let explicitCredentials: Credentials | undefined;
if (accessKeyId && secretAccessKey) {
explicitCredentials = new Credentials({
accessKeyId,
secretAccessKey,
});
}
const roleArn = config.roleArn;
if (roleArn) {
return new aws.ChainableTemporaryCredentials({
masterCredentials: explicitCredentials,
params: {
RoleArn: roleArn,
RoleSessionName: roleSessionName,
},
});
}
return explicitCredentials;
}
}
@@ -21,3 +21,4 @@
*/
export * from './processors';
export * from './providers';
@@ -0,0 +1,196 @@
/*
* 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 { getVoidLogger } from '@backstage/backend-common';
import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks';
import { ConfigReader } from '@backstage/config';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
import { AwsS3EntityProvider } from './AwsS3EntityProvider';
import aws from 'aws-sdk';
import AWSMock from 'aws-sdk-mock';
class PersistingTaskRunner implements TaskRunner {
private tasks: TaskInvocationDefinition[] = [];
getTasks() {
return this.tasks;
}
run(task: TaskInvocationDefinition): Promise<void> {
this.tasks.push(task);
return Promise.resolve(undefined);
}
}
const logger = getVoidLogger();
describe('AwsS3EntityProvider', () => {
const config = new ConfigReader({
catalog: {
providers: {
awsS3: {
anyProviderId: {
bucketName: 'bucket-1',
region: 'us-east-1',
prefix: 'sub/dir/',
},
},
},
},
});
const schedule = new PersistingTaskRunner();
AWSMock.setSDKInstance(aws);
const createObjectList = (...keys: string[]): aws.S3.ObjectList => {
const objects = keys.map(key => {
return {
Key: key,
} as aws.S3.Types.Object;
});
return objects as aws.S3.ObjectList;
};
AWSMock.mock('S3', 'listObjectsV2', async req => {
const prefix = req.Prefix ?? '';
if (!req.ContinuationToken) {
return {
Contents: createObjectList(`${prefix}key1.yaml`, `${prefix}key2.yaml`),
NextContinuationToken: 'next-token',
} as aws.S3.Types.ListObjectsV2Output;
}
return {
Contents: createObjectList(`${prefix}key3.yaml`, `${prefix}key4.yaml`),
} as aws.S3.Types.ListObjectsV2Output;
});
afterEach(() => jest.resetAllMocks());
it('apply full update on scheduled execution', async () => {
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
};
const provider = AwsS3EntityProvider.fromConfig(config, {
logger,
schedule,
})[0];
expect(provider.getProviderName()).toEqual('awsS3-provider:anyProviderId');
await provider.connect(entityProviderConnection);
const taskDef = schedule.getTasks()[0];
expect(taskDef.id).toEqual('awsS3-provider:anyProviderId:refresh');
await (taskDef.fn as () => Promise<void>)();
expect(entityProviderConnection.applyMutation).toBeCalledWith({
type: 'full',
entities: [
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key1.yaml',
'backstage.io/managed-by-origin-location':
'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key1.yaml',
},
name: 'generated-980e6ad47fbfbfeead708a9c7c87331b7540296a',
},
spec: {
presence: 'required',
target:
'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key1.yaml',
type: 'url',
},
},
locationKey: 'awsS3-provider:anyProviderId',
},
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key2.yaml',
'backstage.io/managed-by-origin-location':
'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key2.yaml',
},
name: 'generated-266794d8e789089dddba2b42cd79e70b149aa61c',
},
spec: {
presence: 'required',
target:
'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key2.yaml',
type: 'url',
},
},
locationKey: 'awsS3-provider:anyProviderId',
},
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key3.yaml',
'backstage.io/managed-by-origin-location':
'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key3.yaml',
},
name: 'generated-96f0cdcd7e33aa687c19d160ec7d5b1975cb9ea1',
},
spec: {
presence: 'required',
target:
'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key3.yaml',
type: 'url',
},
},
locationKey: 'awsS3-provider:anyProviderId',
},
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key4.yaml',
'backstage.io/managed-by-origin-location':
'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key4.yaml',
},
name: 'generated-cd1a799b5ecfc055a0c672654420af3afeb648d3',
},
spec: {
presence: 'required',
target:
'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key4.yaml',
type: 'url',
},
},
locationKey: 'awsS3-provider:anyProviderId',
},
],
});
});
});
@@ -0,0 +1,211 @@
/*
* 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 { TaskRunner } from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
import { AwsS3Integration, ScmIntegrations } from '@backstage/integration';
import {
EntityProvider,
EntityProviderConnection,
LocationSpec,
locationSpecToLocationEntity,
} from '@backstage/plugin-catalog-backend';
import { AwsCredentials } from '../credentials/AwsCredentials';
import { readAwsS3Configs } from './config';
import { AwsS3Config } from './types';
import { S3 } from 'aws-sdk';
import { ListObjectsV2Output } from 'aws-sdk/clients/s3';
import * as uuid from 'uuid';
import { Logger } from 'winston';
// TODO: event-based updates using S3 events (+ queue like SQS)?
/**
* Provider which discovers catalog files (any name) within an S3 bucket.
*
* Use `AwsS3EntityProvider.fromConfig(...)` to create instances.
*
* @public
*/
export class AwsS3EntityProvider implements EntityProvider {
private readonly logger: Logger;
private readonly s3: S3;
private readonly scheduleFn: () => Promise<void>;
private connection?: EntityProviderConnection;
static fromConfig(
configRoot: Config,
options: {
logger: Logger;
schedule: TaskRunner;
},
): AwsS3EntityProvider[] {
const providerConfigs = readAwsS3Configs(configRoot);
// Even though the awsS3 integration allows a config array
// there is no *real* support for multiple configs.
// Usually, there will be just the integration for the default host.
// In case, a config custom endpoint is used, the host from this endpoint
// will be extracted and used as host (e.g., localhost when used with LocalStack)
// and the default integration will be added as second integration.
// In this case, we still want the first one though, but have no means to select it
// just from the bucket name (and region).
const integration = ScmIntegrations.fromConfig(configRoot).awsS3.list()[0];
if (!integration) {
throw new Error('No integration found for awsS3');
}
return providerConfigs.map(
providerConfig =>
new AwsS3EntityProvider(
providerConfig,
integration,
options.logger,
options.schedule,
),
);
}
private constructor(
private readonly config: AwsS3Config,
private readonly integration: AwsS3Integration,
logger: Logger,
schedule: TaskRunner,
) {
this.logger = logger.child({
target: this.getProviderName(),
});
this.s3 = new S3({
apiVersion: '2006-03-01',
credentials: AwsCredentials.create(
integration.config,
'backstage-aws-s3-provider',
),
endpoint: integration.config.endpoint,
region: this.config.region,
s3ForcePathStyle: integration.config.s3ForcePathStyle,
});
this.scheduleFn = this.createScheduleFn(schedule);
}
private createScheduleFn(schedule: TaskRunner): () => Promise<void> {
return async () => {
const taskId = `${this.getProviderName()}:refresh`;
return schedule.run({
id: taskId,
fn: async () => {
const logger = this.logger.child({
class: AwsS3EntityProvider.prototype.constructor.name,
taskId,
taskInstanceId: uuid.v4(),
});
try {
await this.refresh(logger);
} catch (error) {
logger.error(error);
}
},
});
};
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
getProviderName(): string {
return `awsS3-provider:${this.config.id}`;
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
await this.scheduleFn();
}
async refresh(logger: Logger) {
if (!this.connection) {
throw new Error('Not initialized');
}
logger.info('Discovering AWS S3 objects');
const keys = await this.listAllObjectKeys();
logger.info(`Discovered ${keys.length} AWS S3 objects`);
const locations = keys.map(key => this.createLocationSpec(key));
await this.connection.applyMutation({
type: 'full',
entities: locations.map(location => {
return {
locationKey: this.getProviderName(),
entity: locationSpecToLocationEntity({ location }),
};
}),
});
logger.info(`Committed ${locations.length} Locations for AWS S3 objects`);
}
private async listAllObjectKeys(): Promise<string[]> {
const keys: string[] = [];
let continuationToken: string | undefined = undefined;
let output: ListObjectsV2Output;
do {
const request = this.s3.listObjectsV2({
Bucket: this.config.bucketName,
ContinuationToken: continuationToken,
Prefix: this.config.prefix,
});
output = await request.promise();
if (output.Contents) {
output.Contents.forEach(item => {
if (item.Key && !item.Key.endsWith('/')) {
keys.push(item.Key);
}
});
}
continuationToken = output.NextContinuationToken;
} while (continuationToken);
return keys;
}
private createLocationSpec(key: string): LocationSpec {
return {
type: 'url',
target: this.createObjectUrl(key),
presence: 'required',
};
}
private createObjectUrl(key: string): string {
const bucketName = this.config.bucketName;
const endpoint = this.integration.config.endpoint;
if (endpoint) {
if (endpoint.startsWith(`https://${bucketName}.`)) {
return `${endpoint}/${key}`;
}
return `${endpoint}/${bucketName}/${key}`;
}
return `https://${bucketName}.s3.${this.config.region}.amazonaws.com/${key}`;
}
}
@@ -0,0 +1,98 @@
/*
* Copyright 2020 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 { ConfigReader } from '@backstage/config';
import { readAwsS3Configs } from './config';
describe('readAwsS3Configs', () => {
it('reads single provider config', () => {
const provider = {
bucketName: 'bucket-1',
region: 'us-east-1',
prefix: 'sub/dir/',
};
const config = {
catalog: {
providers: {
awsS3: provider,
},
},
};
const actual = readAwsS3Configs(new ConfigReader(config));
expect(actual).toHaveLength(1);
expect(actual[0]).toEqual({
...provider,
id: 'default',
});
});
it('reads all provider configs', () => {
const provider1 = {
bucketName: 'bucket-1',
region: 'us-east-1',
prefix: 'sub/dir/',
};
const provider2 = {
bucketName: 'bucket-2',
region: 'eu-west-1',
};
const provider3 = {
bucketName: 'bucket-3',
};
const config = {
catalog: {
providers: {
awsS3: { provider1, provider2, provider3 },
},
},
};
const actual = readAwsS3Configs(new ConfigReader(config));
expect(actual).toHaveLength(3);
expect(actual[0]).toEqual({
...provider1,
id: 'provider1',
});
expect(actual[1]).toEqual({
...provider2,
id: 'provider2',
});
expect(actual[2]).toEqual({
...provider3,
id: 'provider3',
});
});
it('fails if bucketName is missing', () => {
const provider = {
region: 'us-east-1',
};
const config = {
catalog: {
providers: {
awsS3: { provider },
},
},
};
expect(() => readAwsS3Configs(new ConfigReader(config))).toThrow(
"Missing required config value at 'catalog.providers.awsS3.provider.bucketName'",
);
});
});
@@ -0,0 +1,55 @@
/*
* 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 { Config } from '@backstage/config';
import { AwsS3Config } from './types';
const DEFAULT_PROVIDER_ID = 'default';
export function readAwsS3Configs(config: Config): AwsS3Config[] {
const configs: AwsS3Config[] = [];
const providerConfigs = config.getOptionalConfig('catalog.providers.awsS3');
if (!providerConfigs) {
return configs;
}
if (providerConfigs.has('bucketName')) {
// simple/single config variant
configs.push(readAwsS3Config(DEFAULT_PROVIDER_ID, providerConfigs));
return configs;
}
for (const id of providerConfigs.keys()) {
configs.push(readAwsS3Config(id, providerConfigs.getConfig(id)));
}
return configs;
}
function readAwsS3Config(id: string, config: Config): AwsS3Config {
const bucketName = config.getString('bucketName');
const region = config.getOptionalString('region');
const prefix = config.getOptionalString('prefix');
return {
id,
bucketName,
region,
prefix,
};
}
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { AwsS3EntityProvider } from './AwsS3EntityProvider';
@@ -0,0 +1,22 @@
/*
* 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.
*/
export type AwsS3Config = {
id: string;
bucketName: string;
prefix?: string;
region?: string;
};
@@ -72,6 +72,9 @@ catalog:
# Optional search for groups, see Microsoft Graph API for the syntax
# See https://docs.microsoft.com/en-us/graph/search-query-parameter
groupSearch: '"description:One" AND ("displayName:Video" OR "displayName:Drive")'
# Optional select for groups, this will allow you work with schemaExtensions in order to add extra information to your groups that can be used on you custom groupTransformers
# See https://docs.microsoft.com/en-us/graph/api/resources/schemaextension?view=graph-rest-1.0
groupSelect: ['id', 'displayName', 'description']
```
`userFilter` and `userGroupMemberFilter` are mutually exclusive, only one can be provided. If both are provided, an error will be thrown.
@@ -182,6 +182,7 @@ export type MicrosoftGraphProviderConfig = {
groupExpand?: string;
groupFilter?: string;
groupSearch?: string;
groupSelect?: string[];
queryMode?: 'basic' | 'advanced';
};
@@ -219,6 +220,7 @@ export function readMicrosoftGraphOrg(
groupExpand?: string;
groupSearch?: string;
groupFilter?: string;
groupSelect?: string[];
queryMode?: 'basic' | 'advanced';
userTransformer?: UserTransformer;
groupTransformer?: GroupTransformer;
+8
View File
@@ -79,6 +79,14 @@ export interface Config {
* E.g. "\"displayName:-team\"" would only match groups which contain '-team'
*/
groupSearch?: string;
/**
* The fields to be fetched on query.
*
* E.g. ["id", "displayName", "description"]
*/
groupSelect?: string[];
/**
* The filter to apply to extract users by groups memberships.
*
@@ -56,6 +56,7 @@ describe('readMicrosoftGraphConfig', () => {
userExpand: 'manager',
userFilter: 'accountEnabled eq true',
groupExpand: 'member',
groupSelect: ['id', 'displayName', 'description'],
groupFilter: 'securityEnabled eq false',
},
],
@@ -71,6 +72,7 @@ describe('readMicrosoftGraphConfig', () => {
userExpand: 'manager',
userFilter: 'accountEnabled eq true',
groupExpand: 'member',
groupSelect: ['id', 'displayName', 'description'],
groupFilter: 'securityEnabled eq false',
},
];
@@ -88,6 +88,14 @@ export type MicrosoftGraphProviderConfig = {
* E.g. "\"displayName:-team\"" would only match groups which contain '-team'
*/
groupSearch?: string;
/**
* The fields to be fetched on query.
*
* E.g. ["id", "displayName", "description"]
*/
groupSelect?: string[];
/**
* By default, the Microsoft Graph API only provides the basic feature set
* for querying. Certain features are limited to advanced query capabilities
@@ -145,6 +153,7 @@ export function readMicrosoftGraphConfig(
);
}
const groupSelect = providerConfig.getOptionalStringArray('groupSelect');
const queryMode = providerConfig.getOptionalString('queryMode');
if (
queryMode !== undefined &&
@@ -167,6 +176,7 @@ export function readMicrosoftGraphConfig(
groupExpand,
groupFilter,
groupSearch,
groupSelect,
queryMode,
});
}
@@ -343,6 +343,7 @@ export async function readMicrosoftGraphGroups(
groupExpand?: string;
groupFilter?: string;
groupSearch?: string;
groupSelect?: string[];
groupTransformer?: GroupTransformer;
organizationTransformer?: OrganizationTransformer;
},
@@ -373,6 +374,7 @@ export async function readMicrosoftGraphGroups(
expand: options?.groupExpand,
search: options?.groupSearch,
filter: options?.groupFilter,
select: options?.groupSelect,
},
options?.queryMode,
)) {
@@ -535,6 +537,7 @@ export async function readMicrosoftGraphOrg(
groupExpand?: string;
groupSearch?: string;
groupFilter?: string;
groupSelect?: string[];
queryMode?: 'basic' | 'advanced';
userTransformer?: UserTransformer;
groupTransformer?: GroupTransformer;
@@ -571,6 +574,7 @@ export async function readMicrosoftGraphOrg(
queryMode: options.queryMode,
groupSearch: options.groupSearch,
groupFilter: options.groupFilter,
groupSelect: options.groupSelect,
groupTransformer: options.groupTransformer,
organizationTransformer: options.organizationTransformer,
});
@@ -176,7 +176,6 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
const provider = this.options.provider;
const { markReadComplete } = trackProgress(logger);
const client = MicrosoftGraphClient.create(this.options.provider);
const { users, groups } = await readMicrosoftGraphOrg(
client,
provider.tenantId,
@@ -186,6 +185,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
userGroupMemberSearch: provider.userGroupMemberSearch,
groupFilter: provider.groupFilter,
groupSearch: provider.groupSearch,
groupSelect: provider.groupSelect,
queryMode: provider.queryMode,
groupTransformer: this.options.groupTransformer,
userTransformer: this.options.userTransformer,
@@ -113,6 +113,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
groupExpand: provider.groupExpand,
groupFilter: provider.groupFilter,
groupSearch: provider.groupSearch,
groupSelect: provider.groupSelect,
queryMode: provider.queryMode,
userTransformer: this.userTransformer,
groupTransformer: this.groupTransformer,
+2 -1
View File
@@ -22,6 +22,7 @@ import { Permission } from '@backstage/plugin-permission-common';
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
import { PermissionCondition } from '@backstage/plugin-permission-common';
import { PermissionCriteria } from '@backstage/plugin-permission-common';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { PermissionRule } from '@backstage/plugin-permission-node';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
@@ -181,7 +182,7 @@ export type CatalogEnvironment = {
database: PluginDatabaseManager;
config: Config;
reader: UrlReader;
permissions: PermissionAuthorizer;
permissions: PermissionEvaluator | PermissionAuthorizer;
};
// @alpha
@@ -23,11 +23,7 @@ import {
CatalogClient,
GetEntitiesRequest,
} from '@backstage/catalog-client';
import {
Entity,
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
import {
@@ -36,6 +32,7 @@ import {
} from '@backstage/plugin-catalog-common';
import { Permission } from '@backstage/plugin-permission-common';
import { Readable } from 'stream';
import { getDocumentText } from './util';
/** @public */
export type DefaultCatalogCollatorFactoryOptions = {
@@ -100,24 +97,6 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory {
return formatted.toLowerCase();
}
private isUserEntity(entity: Entity): entity is UserEntity {
return entity.kind.toLocaleUpperCase('en-US') === 'USER';
}
private getDocumentText(entity: Entity): string {
let documentText = entity.metadata.description || '';
if (this.isUserEntity(entity)) {
if (entity.spec?.profile?.displayName && documentText) {
// combine displayName and description
const displayName = entity.spec?.profile?.displayName;
documentText = displayName.concat(' : ', documentText);
} else {
documentText = entity.spec?.profile?.displayName || documentText;
}
}
return documentText;
}
private async *execute(): AsyncGenerator<CatalogEntityDocument> {
const { token } = await this.tokenManager.getToken();
let entitiesRetrieved = 0;
@@ -150,7 +129,7 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory {
kind: entity.kind,
name: entity.metadata.name,
}),
text: this.getDocumentText(entity),
text: getDocumentText(entity),
componentType: entity.spec?.type?.toString() || 'other',
type: entity.spec?.type?.toString() || 'other',
namespace: entity.metadata.namespace || 'default',
@@ -0,0 +1,146 @@
/*
* 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 {
ComponentEntity,
GroupEntity,
UserEntity,
} from '@backstage/catalog-model';
import { getDocumentText } from './util';
describe('getDocumentText', () => {
describe('kind is not User or Group', () => {
test('contains description if set', () => {
const entity = createComponent();
entity.metadata.description = 'The expected description';
const actual = getDocumentText(entity);
expect(actual).toContain(entity.metadata.description);
});
test('is empty if description is not set', () => {
const entity = createComponent();
const actual = getDocumentText(entity);
expect(actual).toEqual('');
});
});
describe('kind is User', () => {
test('contains display name if set', () => {
const entity = createUser();
const actual = getDocumentText(entity);
expect(actual).toContain(entity.spec.profile?.displayName);
});
test('contains description if set', () => {
const entity = createUser();
const actual = getDocumentText(entity);
expect(actual).toContain(entity.metadata.description);
});
test('contains both description and display name if both are set', () => {
const entity = createUser();
const actual = getDocumentText(entity);
expect(actual).toContain(entity.spec.profile?.displayName);
expect(actual).toContain(entity.metadata.description);
});
test('is empty if description and display name are not set', () => {
const entity = createUser();
delete entity.metadata.description;
delete entity.spec.profile?.displayName;
const actual = getDocumentText(entity);
expect(actual).toEqual('');
});
});
describe('kind is Group', () => {
test('contains display name if set', () => {
const entity = createGroup();
const actual = getDocumentText(entity);
expect(actual).toContain(entity.spec.profile?.displayName);
});
test('contains description if set', () => {
const entity = createGroup();
const actual = getDocumentText(entity);
expect(actual).toContain(entity.metadata.description);
});
test('contains both description and display name if both are set', () => {
const entity = createGroup();
const actual = getDocumentText(entity);
expect(actual).toContain(entity.spec.profile?.displayName);
expect(actual).toContain(entity.metadata.description);
});
test('is empty if description and display name are not set', () => {
const entity = createGroup();
delete entity.metadata.description;
delete entity.spec.profile?.displayName;
const actual = getDocumentText(entity);
expect(actual).toEqual('');
});
});
});
function createGroup(): GroupEntity {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'group-1',
description: 'The expected description',
},
spec: {
type: 'team',
profile: {
displayName: 'Group 1',
},
children: [],
},
};
}
function createUser(): UserEntity {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: 'user-1',
description: 'The expected description',
},
spec: {
profile: {
displayName: 'User 1',
},
},
};
}
function createComponent(): ComponentEntity {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'component-1',
},
spec: {
lifecycle: 'experimental',
owner: 'someone',
type: 'service',
},
};
}

Some files were not shown because too many files have changed in this diff Show More