Merge branch 'master' into feat/customSearchModal
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-home': patch
|
||||
---
|
||||
|
||||
Export template logos `TemplateBackstageLogo` and `TemplateBackstageLogoIcon` from package.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
imports from `@backstage/plugin-search-react` instead of `@backstage/plugin-search`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Bump the `rushstack` api generator libraries to their latest versions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Use new `PermissionEvaluator#authorizeConditional` method when retrieving permission conditions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
---
|
||||
|
||||
build(deps): bump `cronstrue` from 1.125.0 to 2.2.0
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kafka-backend': patch
|
||||
---
|
||||
|
||||
build(deps-dev): bump `@types/jest-when` from 2.7.2 to 3.5.0
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
build(deps): bump `npm-packlist` from 3.0.0 to 5.0.0
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend': patch
|
||||
---
|
||||
|
||||
Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Changed input label for owner field in GitlabRepoPicker
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins-backend': patch
|
||||
---
|
||||
|
||||
Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated.
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-todo-backend': patch
|
||||
---
|
||||
|
||||
Fix method to get source-location.
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-aws': patch
|
||||
---
|
||||
|
||||
Corrected title and URL to setup documentation in README
|
||||
@@ -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",
|
||||
```
|
||||
@@ -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"
|
||||
},
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Added `spec.profile.displayName` to search index for Group kinds
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
/* ... */
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Defensively encode URL parameters when fetching ELB keys
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Fixed a bug were the `react-hot-loader` transform was being applied to backend development builds.
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
};
|
||||
```
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-home': patch
|
||||
---
|
||||
|
||||
Updated the dependency on `@backstage/config` to `^1.0.0`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': patch
|
||||
---
|
||||
|
||||
Add dedicated gRPC api definition widget
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-permission-react': patch
|
||||
---
|
||||
|
||||
**BREAKING:** Make `IdentityPermissionApi#authorize` typing more strict, using `AuthorizePermissionRequest` and `AuthorizePermissionResponse`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/config-loader': minor
|
||||
---
|
||||
|
||||
Added `ignoreSchemaErrors` to `schema.process`.
|
||||
@@ -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`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-gocd': patch
|
||||
---
|
||||
|
||||
Add DORA metrics insights to GoCD builds page
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-insights': minor
|
||||
---
|
||||
|
||||
Add checksId option to EntityTechInsightsScorecardContent component
|
||||
@@ -39,6 +39,7 @@ Changesets
|
||||
chanwit
|
||||
Chanwit
|
||||
ci
|
||||
CI/CD
|
||||
classname
|
||||
cli
|
||||
cloudbuild
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 team’s 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="<mxfile><diagram id="M4OCM2KiCGRnt6vHj1W_" name="Page-1">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=</diagram></mxfile>" 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 <plugin>-react, and backend exports in <plugin>-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 <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 <plugin>-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 <plugin>-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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -10,29 +10,20 @@ description: TechDocs is Spotify’s homegrown docs-like-code solution built dir
|
||||
|
||||
<!-- Intro, backstory, etc.: -->
|
||||
|
||||
TechDocs is Spotify’s 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 Spotify’s 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 Spotify’s 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 Spotify’s 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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## Databases
|
||||
|
||||
As we have seen, both the `lighthouse-audit-service` and `catalog-backend`
|
||||
|
||||
@@ -153,6 +153,11 @@
|
||||
"label": "Datadog",
|
||||
"ids": ["integrations/datadog-rum/installation"]
|
||||
},
|
||||
{
|
||||
"type": "subcategory",
|
||||
"label": "Gerrit",
|
||||
"ids": ["integrations/gerrit/locations"]
|
||||
},
|
||||
{
|
||||
"type": "subcategory",
|
||||
"label": "GitHub",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+6
-10
@@ -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,
|
||||
@@ -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>();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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' }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
+12
-5
@@ -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} />
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+32
@@ -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' }));
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user