Merge branch 'master' into useLongPollingLogs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-user-settings': patch
|
||||
---
|
||||
|
||||
Handle errors that may occur when the user logs out
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': minor
|
||||
---
|
||||
|
||||
Bitbucket Cloud provider: Add option to configure schedule via `app-config.yaml` instead of in code.
|
||||
|
||||
Please find how to configure the schedule at the config at
|
||||
https://backstage.io/docs/integrations/bitbucketCloud/discovery
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': minor
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Implemented the visual parts of `EntityKindPicker` so that it can be shown alongside the other filters on the left side of your catalog pages.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Fixed a bug where gathered index routes would fail to bind routable extensions. This would typically show up when placing a routable extension in the entity page overview tab.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Tweak the Jest Caching loader to only operate when in `watch` mode
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': minor
|
||||
---
|
||||
|
||||
Added new column `Label` to `CatalogTable.columns`, this new column allows you make use of labels from metadata.
|
||||
For example: category and visibility are type of labels associated with API entity illustrated below.
|
||||
|
||||
YAML code snippet for API entity
|
||||
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: API
|
||||
metadata:
|
||||
name: sample-api
|
||||
description: API for sample
|
||||
links:
|
||||
- url: http://localhost:8080/swagger-ui.html
|
||||
title: Swagger UI
|
||||
tags:
|
||||
- http
|
||||
labels:
|
||||
category: legacy
|
||||
visibility: protected
|
||||
```
|
||||
|
||||
Consumers can customise columns to include label column and show in api-docs list
|
||||
|
||||
```typescript
|
||||
const columns = [
|
||||
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
|
||||
CatalogTable.columns.createLabelColumn('category', { title: 'Category' }),
|
||||
CatalogTable.columns.createLabelColumn('visibility', {
|
||||
title: 'Visibility',
|
||||
defaultValue: 'public',
|
||||
}),
|
||||
];
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-tasks': patch
|
||||
---
|
||||
|
||||
Added new function `readTaskScheduleDefinitionFromConfig` to read `TaskScheduleDefinition` (aka. schedule) from the `Config`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch
|
||||
---
|
||||
|
||||
Add `bitbucketCloudCatalogModule` (new backend-plugin-api, alpha).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Fix BitBucket server integration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': minor
|
||||
---
|
||||
|
||||
Google OAuth refresh tokens will now be revoked on logout by calling Google's API
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-bazaar': patch
|
||||
---
|
||||
|
||||
Link to the user catalog entity of a member
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-user-settings-backend': patch
|
||||
---
|
||||
|
||||
Use `Response.status` instead of `.send(number)`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
Update the `github:publish` action to allow passing whether pull
|
||||
requests must be up to date with the default branch before merging.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-vault-backend': patch
|
||||
---
|
||||
|
||||
VaultBuilder.tsx renamed to VaultBuilder in order for module to be correctly loaded.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Fixed a bug where the `allowed*` values for the `RepoUrlPicker` would be reset on render.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Fixed an issue where `getClient()` for a `pluginId` would return different clients and not share them
|
||||
+35
-1
@@ -176,34 +176,65 @@
|
||||
"@backstage/plugin-xcmetrics": "0.2.29"
|
||||
},
|
||||
"changesets": [
|
||||
"blue-lions-greet",
|
||||
"brave-peaches-brush",
|
||||
"breezy-pots-worry",
|
||||
"bright-rules-shout",
|
||||
"calm-moose-fetch",
|
||||
"calm-pianos-burn",
|
||||
"clean-camels-sneeze",
|
||||
"clever-ties-burn",
|
||||
"cuddly-bikes-tease",
|
||||
"curly-rats-itch",
|
||||
"curvy-kiwis-fold",
|
||||
"curvy-lemons-change",
|
||||
"dry-shirts-attack",
|
||||
"dull-rocks-warn",
|
||||
"eleven-apples-accept",
|
||||
"fair-tools-melt",
|
||||
"fifty-berries-learn",
|
||||
"grumpy-pans-knock",
|
||||
"hot-geese-vanish",
|
||||
"itchy-schools-run",
|
||||
"kind-penguins-report",
|
||||
"large-dingos-juggle",
|
||||
"lazy-beds-pull",
|
||||
"lazy-fireants-check",
|
||||
"little-roses-rule",
|
||||
"loud-dots-sit",
|
||||
"lovely-peaches-fold",
|
||||
"lucky-cows-boil",
|
||||
"mean-spiders-design",
|
||||
"mighty-lions-march",
|
||||
"odd-pandas-suffer",
|
||||
"odd-singers-taste",
|
||||
"old-cobras-suffer",
|
||||
"old-needles-brake",
|
||||
"perfect-moose-drum",
|
||||
"plenty-kids-fetch",
|
||||
"plenty-laws-end",
|
||||
"poor-clouds-ring",
|
||||
"pretty-buttons-develop",
|
||||
"quiet-dancers-jog",
|
||||
"quiet-hats-kick",
|
||||
"quiet-ligers-draw",
|
||||
"rare-hotels-cough",
|
||||
"red-dots-sleep",
|
||||
"red-pants-rush",
|
||||
"renovate-5da63a4",
|
||||
"renovate-a02d90b",
|
||||
"rich-carrots-reflect",
|
||||
"rude-bulldogs-sleep",
|
||||
"search-bobcats-love",
|
||||
"search-cars-hide",
|
||||
"search-clouds-begin",
|
||||
"search-cycles-sniff",
|
||||
"search-dull-planes-prove",
|
||||
"search-rats-grin",
|
||||
"selfish-turkeys-exist",
|
||||
"shaggy-books-smell",
|
||||
"silent-bees-repeat",
|
||||
"silly-rules-join",
|
||||
"sixty-items-nail",
|
||||
"slow-mirrors-eat",
|
||||
"smooth-tables-pull",
|
||||
@@ -217,8 +248,11 @@
|
||||
"thick-kings-destroy",
|
||||
"tiny-mails-bathe",
|
||||
"tough-hairs-sparkle",
|
||||
"twelve-melons-notice",
|
||||
"warm-days-watch",
|
||||
"wild-weeks-live",
|
||||
"wise-ligers-scream",
|
||||
"yellow-lemons-march"
|
||||
"yellow-lemons-march",
|
||||
"young-bugs-remember"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Auth0 provider now supports optional `connection` and `connectionScope` parameters to configure social identity providers.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-github': patch
|
||||
---
|
||||
|
||||
Add missing config schema for the `GitHubEntityProvider`.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/cli': patch
|
||||
'@backstage/plugin-airbrake-backend': patch
|
||||
'@backstage/plugin-badges-backend': patch
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
'@backstage/plugin-graphql-backend': patch
|
||||
'@backstage/plugin-periskop-backend': patch
|
||||
'@backstage/plugin-permission-backend': patch
|
||||
'@backstage/plugin-rollbar-backend': patch
|
||||
'@backstage/plugin-search-backend': patch
|
||||
'@backstage/plugin-tech-insights-backend': patch
|
||||
'@backstage/plugin-user-settings-backend': patch
|
||||
---
|
||||
|
||||
Use `response.json` rather than `response.send` where appropriate, as outlined in `SECURITY.md`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added documentation for `createBackendModule`, with guidelines for choosing a module ID.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Switching off duplicated timestamp in case of logging via task logger in a custom action
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': minor
|
||||
---
|
||||
|
||||
The search query state now has an optional `pageLimit` property that determines how many results will be requested per page, it defaults to 25.
|
||||
|
||||
Examples:
|
||||
_Basic_
|
||||
|
||||
```jsx
|
||||
<SearchResults query={{ pageLimit: 30 }}>
|
||||
{results => {
|
||||
// Item rendering logic is omitted
|
||||
}}
|
||||
</SearchResults>
|
||||
```
|
||||
|
||||
_With context_
|
||||
|
||||
```jsx
|
||||
<SearchContextProvider initialState={{ pageLimit: 30 }}>
|
||||
<SearchResults>
|
||||
{results => {
|
||||
// Item rendering logic is omitted
|
||||
}}
|
||||
</SearchResults>
|
||||
</SearchContextProvider>
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-common': minor
|
||||
---
|
||||
|
||||
There is a new property called `pageLimit` on the `SearchQuery` interface that specifies how many results should be returned per page.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': minor
|
||||
---
|
||||
|
||||
The `<SearchResultList />` component now accepts an optional property `disableRenderingWithNoResults` to disable rendering when no results are returned.
|
||||
Possibility to provide a custom no results component if needed through the `noResultsComponent` property.
|
||||
|
||||
Examples:
|
||||
|
||||
_Rendering a custom no results component_
|
||||
|
||||
```jsx
|
||||
<SearchResultList
|
||||
query={query}
|
||||
noResultsComponent={<ListItemText primary="No results were found" />}
|
||||
/>
|
||||
```
|
||||
|
||||
_Disable rendering when there are no results_
|
||||
|
||||
```jsx
|
||||
<SearchResultList query={query} disableRenderingWithNoResults />
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-node': patch
|
||||
'@backstage/plugin-search-backend-module-pg': patch
|
||||
'@backstage/plugin-search-backend-module-elasticsearch': patch
|
||||
---
|
||||
|
||||
The search engine has been updated to take advantage of the `pageLimit` property on search queries. If none is provided, the search engine will continue to use its default value of 25 results per page.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': minor
|
||||
---
|
||||
|
||||
The `<SearchResultGroup />` component now accepts an optional property `disableRenderingWithNoResults` to disable rendering when no results are returned.
|
||||
Possibility to provide a custom no results component if needed through the `noResultsComponent` property.
|
||||
|
||||
Examples:
|
||||
|
||||
_Rendering a custom no results component_
|
||||
|
||||
```jsx
|
||||
<SearchResultGroup
|
||||
query={query}
|
||||
icon={<DocsIcon />}
|
||||
title="Documentation"
|
||||
noResultsComponent={<ListItemText primary="No results were found" />}
|
||||
/>
|
||||
```
|
||||
|
||||
_Disable rendering when there are no results_
|
||||
|
||||
```jsx
|
||||
<SearchResultGroup
|
||||
query={query}
|
||||
icon={<DocsIcon />}
|
||||
title="Documentation"
|
||||
disableRenderingWithNoResults
|
||||
/>
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend': minor
|
||||
---
|
||||
|
||||
The query received by search engines now contains a property called `pageLimit`, it specifies how many results to return per page when sending a query request to the search backend.
|
||||
|
||||
Example:
|
||||
_Returns up to 30 results per page_
|
||||
|
||||
```
|
||||
GET /query?pageLimit=30
|
||||
```
|
||||
|
||||
The search backend validates the page limit and this value must not exceed 100, but it doesn't set a default value for the page limit parameter, it leaves it up to each search engine to set this, so Lunr, Postgres and Elastic Search set 25 results per page as a default value.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Fixed setting default branch for Bitbucket Server
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
'@backstage/plugin-bazaar-backend': minor
|
||||
---
|
||||
|
||||
**BREAKING** The bazaar-backend `createRouter` now requires that the `identityApi` is passed to the router.
|
||||
|
||||
These changes are **required** to `packages/backend/src/plugins/bazaar.ts`
|
||||
|
||||
The user entity ref is now added to the members table and is taken from the requesting user using the `identityApi`.
|
||||
|
||||
```diff
|
||||
import { PluginEnvironment } from '../types';
|
||||
import { createRouter } from '@backstage/plugin-bazaar-backend';
|
||||
import { Router } from 'express';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
database: env.database,
|
||||
+ identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
Added optional `sourcePath` parameter to `publish:gitlab:merge-request` action, `targetPath` is now optional and falls back to current workspace path.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Added support for copy entity URL in entity page context menu
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-github': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Updated the `moduleId` of the experimental module export.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-github-issues': patch
|
||||
---
|
||||
|
||||
Add filtering and ordering to the graphql query
|
||||
@@ -53,6 +53,10 @@ jobs:
|
||||
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
|
||||
# TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while
|
||||
script: |
|
||||
const releaseVersion = require('./backstage/package.json').version;
|
||||
if(releaseVersion.includes('next')) {
|
||||
return;
|
||||
}
|
||||
console.log('Dispatching upgrade helper sync');
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'backstage',
|
||||
@@ -61,6 +65,6 @@ jobs:
|
||||
ref: 'master',
|
||||
inputs: {
|
||||
version: require('./backstage/packages/create-app/package.json').version,
|
||||
releaseVersion: require('./backstage/package.json').version
|
||||
releaseVersion
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ jobs:
|
||||
--remote-repo-url=https://github.com/backstage/backstage
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
# Above we run the `monitor` command, this runs the `test` command which is
|
||||
# the one that generates the SARIF report that we can upload to GitHub.
|
||||
@@ -44,6 +45,7 @@ jobs:
|
||||
--sarif-file-output=snyk.sarif
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
- name: Upload Snyk report
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
|
||||
+10
-10
@@ -1,7 +1,7 @@
|
||||
# Adopters
|
||||
|
||||
_If you're using Backstage in your organization, please try to add your company name to this list. It really helps the project to gain momentum and credibility. It's a small contribution back to the project with a big impact._
|
||||
_You can do this by using the [Adopter form](https://form.typeform.com/to/zcOaKikB) or by editing this file after following the [CONTRIBUTING.md](./CONTRIBUTING.md)._
|
||||
_You can do this by using the [Adopter form](https://info.backstage.spotify.com/public-adopter-hand-raiser-form) or by editing this file after following the [CONTRIBUTING.md](./CONTRIBUTING.md)._
|
||||
|
||||
| Organization | Contact | Description of Use |
|
||||
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -91,7 +91,7 @@ _You can do this by using the [Adopter form](https://form.typeform.com/to/zcOaKi
|
||||
| [HBO Max](https://hbomax.com) | [@mdb](https://github.com/mdb), [@nesta219](https://github.com/nesta219), [@nmische](https://github.com/nmische), [@hbomark](https://github.com/hbomark) | Developer portal hosting service catalog and API documentation, as well as cloud infrastructure details, operational visibility tools, and a custom plugin for browsing notable platform change events, such as deployments and configuration updates. |
|
||||
| [RCHLO](https://www.riachuelo.com.br) & [MIDWAY](https://www.midway.com.br) | [@marcosborges](https://github.com/marcosborges), [@defaultbr](https://github.com/defaultbr) | Self-Service Platform |
|
||||
| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. |
|
||||
| [VMware](https://www.vmware.com) | [Waldir Montoya](https://github.com/waldirmontoya25), [Kris Applegate](https://github.com/krisapplegate), [Jamie Klassen](https://github.com/jamieklassen) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal |
|
||||
| [VMware](https://www.vmware.com) | [Waldir Montoya](https://github.com/waldirmontoya25), [Kris Applegate](https://github.com/krisapplegate), [Jamie Klassen](https://github.com/jamieklassen) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal |
|
||||
| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality |
|
||||
| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. |
|
||||
| [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. |
|
||||
@@ -171,7 +171,7 @@ _You can do this by using the [Adopter form](https://form.typeform.com/to/zcOaKi
|
||||
| [AEB](https://www.aeb.com/) | [David Fankhänel](mailto:dfl@aeb.com) | Central developer platform for creating new apps via templates, getting an overview via software catalog, etc |
|
||||
| [SALTO Systems](https://saltosystems.com) | [Ian Cowley](mailto:i.cowley@saltosystems.com) | Currently using Backstage as an internal documentation portal. |
|
||||
| [Lummo](https://lummo.com) | [Anjul Sahu](mailto:anjul@lummo.com) | We are building the internal developer portal using Backstage and bringing up all integrations and service information at one place. |
|
||||
| [Frontside](https://frontside.com/backstage) | [Taras Mankovski](mailto:taras@frontside.com) | An active community member and contributor, Frontside offers both custom development as well as enterprise support services to help teams get the most out of Backstage for the long-run. |
|
||||
| [Frontside](https://frontside.com/backstage) | [Taras Mankovski](mailto:taras@frontside.com) | An active community member and contributor, Frontside offers both custom development as well as enterprise support services to help teams get the most out of Backstage for the long-run. |
|
||||
| [Stepstone](https://www.stepstone.com/en/) | [Neil Kennedy](mailto:neil.kennedy@stepstone.com) | StepStone is using Backstage to solve problems around ownership and visibility of our applications. We have thousands of repos, multiple legacy systems and a growing platform that is hard to maintain. Backstage is forming the centre of our push to embrace the chaos. |
|
||||
| [idwall](https://idwall.co) | [Rodrigo Catão Araujo](mailto:rodrigo@idwall.co) | Developer Portal for internal engineers to access service catalog, documentation, observability, infrastructure and internal tooling. |
|
||||
| [Jaguar Land Rover](https://www.jaguarlandrover.com) | [Josh Walker](mailto:jwalke18@jaguarlandrover.com) | Users can request a Gitlab user, which creates a commit with the Terraform code. |
|
||||
@@ -202,16 +202,16 @@ _You can do this by using the [Adopter form](https://form.typeform.com/to/zcOaKi
|
||||
| [Skillz](https://skillz.com/) | [Peiman Jafari](https://github.com/peimanja) | Internal developers portal for technical documentations, components ownership and relationship, software templates and integrations with internal tools |
|
||||
| [Telus](https://www.telus.com/en/) | [Leo Li](mailto:leo.li@telus.com), [Laurent Robichaud](mailto:laurent.robichaud@telus.com), [Seb Barre](https://github.com/sbarre) | Simplifying the developer experience through centralized team member portals. Our current focus includes the adoption of Tech Docs, Software Catalog, Software Templates, the plethora of plugins, and contributing features back to Backstage. 🤖 |
|
||||
| [Fidelity Investments](https://fidelity.com) | [Ankita Upadhyay](mailto:ankita.upadhyay@fmr.com) | Getting started with the adoption for Monorepo projects |
|
||||
| [Verisk](https://verisk.com) | [Callen Barton](mailto:#xw_architecture@verisk.com), [Kevin Johnson](mailto:#xw_architecture@verisk.com) | Developer portal to quickly create and deploy microservices. |
|
||||
| [Verisk](https://verisk.com) | [Callen Barton](mailto:#xw_architecture@verisk.com), [Kevin Johnson](mailto:#xw_architecture@verisk.com) | Developer portal to quickly create and deploy microservices. |
|
||||
| [iodigital](https://iodigital.com) | [Jan-Willem Mulder](mailto:jan-willem.mulder@iodigital.com) | Internal developer portal for discovery of applications, projects and teams. Using several plugins like the Software Catalog and Tech Insights for promoting best practices and supporting our SDLC toolchain |
|
||||
| [Fanatics](https://www.fanaticsinc.com/) | [Rory Scott](mailto:rscott@fanatics.com) | Internal Portal consolidating documentation, making it easier to manage applications, internal developer community platform, and self-service cloud infrastructure + pipelines. |
|
||||
| [Appfolio](https://appfolio.com) | [Andy Vaughn](mailto:andy.vaughn@appfolio.com) | Internal software catalog, tech radar, documentation portal to disambiguate software and domain ownership, foster exploration of available developer platform services and tools, improve communication, democratize documentation and knowledge sharing, and coordinate the software lifecycle; all in service of a best-in-class developer experience. |
|
||||
| [isaac](https://isaac.com.br/) | [Leonardo Borges](mailto:leonardo.borges@isaac.com.br), [Ordilei Souza](mailto:ordilei.souza@isaac.com.br) | We're using Backstage as our Internal Developer Portal and main microservices catalog for mapping ownership, health and metrics for each one. |
|
||||
| [Paraná Banco](https://site.paranabanco.com.br/) | [Joao Antunes](mailto:joaopma@pbtech.net.br) | Internal software catalog, documentation and ownership, improve communication, democratize documentation and knowledge sharing, and coordinate the software lifecycle; all in service of a best-in-class developer experience. |
|
||||
| [Stone](https://stone.com.br/) | [Levy Fialho](mailto:lfialho@stone.com.br) | We're using Backstage as our Credit Team Developer Portal and microservices catalog for mapping ownership. We are also using mkdocs for microservices documentation. |
|
||||
| [Paraná Banco](https://site.paranabanco.com.br/) | [Joao Antunes](mailto:joaopma@pbtech.net.br) | Internal software catalog, documentation and ownership, improve communication, democratize documentation and knowledge sharing, and coordinate the software lifecycle; all in service of a best-in-class developer experience. |
|
||||
| [Stone](https://stone.com.br/) | [Levy Fialho](mailto:lfialho@stone.com.br) | We're using Backstage as our Credit Team Developer Portal and microservices catalog for mapping ownership. We are also using mkdocs for microservices documentation. |
|
||||
| [REI](https://www.rei.com/) | [Jen Evans](mailto:jenevan@rei.com) | Developer portal focused on an enterprise-wide app catalog to track ownership and surface APIs. |
|
||||
| [next](https://next.me) | [Devan Jeronimo Nack](mailto:devan.j.nack@next.me), [Everson Crusara](mailto:everson.crusara@next.me), [Thiago Carneiro da Silva](mailto:thiagoc.silva@next.me) | We are building our Internal Developer Portal using Backstage to improve developer's experience by centralizing our services catalog and identifing microservices' ownership. Also we are going to improve Technical Documentation and speed up development using software templates to help squads in creation and deployment of new microservices. |
|
||||
| [Vipps](https://vipps.no) | [Martin Ehrnst](https://github.com/ehrnst) | Vipps use backstage for our service catalog, documentation, and developer portal. Using templates we are able to simplify the developer experience when deploying new services to our platform. |
|
||||
| [Ferrovial](https://ferrovial.com) | [Jose Luis Rosado](mailto:jlrosado@ferrovial.com) | Backstage is helping us to improve and acelerate dev experience helping teams to quickly find technical documentation, infrastructure templates, pipelines, software components and quickstarters that have been developed by our squads in a inner source friendly environment.
|
||||
|
|
||||
|
||||
| [Vipps](https://vipps.no) | [Martin Ehrnst](https://github.com/ehrnst) | Vipps use backstage for our service catalog, documentation, and developer portal. Using templates we are able to simplify the developer experience when deploying new services to our platform. |
|
||||
| [Ferrovial](https://ferrovial.com) | [Jose Luis Rosado](mailto:jlrosado@ferrovial.com) | Backstage is helping us to improve and acelerate dev experience helping teams to quickly find technical documentation, infrastructure templates, pipelines, software components and quickstarters that have been developed by our squads in a inner source friendly environment. |
|
||||
| [Inter&Co](https://bancointer.com.br) | [Arnaud Lanna](https://github.com/arnaudlanna), [Adriano Silva](https://github.com/adrianovss), [Bruno Grossi](https://github.com/begrossi) | We're using Backstage as our internal Developer Portal to catalog and collect repositories and microservices pieces of information like ownership, deployment time, and documentation. |
|
||||
| [StatusNeo](https://statusneo.com/) | [Karan Nangru](mailto:nangru@statusneo.com), [@NishkarshRaj](https://github.com/NishkarshRaj), and [Gaurav Sarien](mailto:gaurav.sarien@statusneo.com) | Harnessing the power of central catalog inventory and self-serving software templates |
|
||||
|
||||
@@ -138,6 +138,11 @@ techdocs:
|
||||
dynatrace:
|
||||
baseUrl: https://your.dynatrace.instance.com
|
||||
|
||||
# Score-cards sample configuration.
|
||||
scorecards:
|
||||
jsonDataUrl: https://raw.githubusercontent.com/Oriflame/backstage-plugins/main/plugins/score-card/sample-data/
|
||||
wikiLinkTemplate: https://link-to-wiki/{id}
|
||||
|
||||
sentry:
|
||||
organization: my-company
|
||||
|
||||
|
||||
@@ -8,30 +8,28 @@ API requests from frontend plugins include an authorization header with a Backst
|
||||
|
||||
As techdocs HTML pages load assets without an Authorization header the code below also sets a token cookie when the user logs in (and when the token is about to expire).
|
||||
|
||||
Create `packages/backend/src/authMiddleware.ts`:
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/index.ts from a create-app deployment
|
||||
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { JWT } from 'jose';
|
||||
import { URL } from 'url';
|
||||
import { SingleHostDiscovery } from '@backstage/backend-common';
|
||||
import type { Config } from '@backstage/config';
|
||||
import {
|
||||
IdentityClient,
|
||||
getBearerTokenFromAuthorizationHeader,
|
||||
IdentityClient,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
|
||||
// ...
|
||||
import { NextFunction, Request, Response, RequestHandler } from 'express';
|
||||
import { decodeJwt } from 'jose';
|
||||
import { URL } from 'url';
|
||||
import { PluginEnvironment } from './types';
|
||||
|
||||
function setTokenCookie(
|
||||
res: Response,
|
||||
options: { token: string; secure: boolean; cookieDomain: string },
|
||||
) {
|
||||
try {
|
||||
const payload = JWT.decode(options.token) as object & {
|
||||
exp: number;
|
||||
};
|
||||
res.cookie(`token`, options.token, {
|
||||
expires: new Date(payload?.exp ? payload?.exp * 1000 : 0),
|
||||
const payload = decodeJwt(options.token);
|
||||
res.cookie('token', options.token, {
|
||||
expires: new Date(payload.exp ? payload.exp * 1000 : 0),
|
||||
secure: options.secure,
|
||||
sameSite: 'lax',
|
||||
domain: options.cookieDomain,
|
||||
@@ -43,9 +41,10 @@ function setTokenCookie(
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// ...
|
||||
|
||||
export const createAuthMiddleware = async (
|
||||
config: Config,
|
||||
appEnv: PluginEnvironment,
|
||||
) => {
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
const identity = IdentityClient.create({
|
||||
discovery,
|
||||
@@ -54,7 +53,7 @@ async function main() {
|
||||
const baseUrl = config.getString('backend.baseUrl');
|
||||
const secure = baseUrl.startsWith('https://');
|
||||
const cookieDomain = new URL(baseUrl).hostname;
|
||||
const authMiddleware = async (
|
||||
const authMiddleware: RequestHandler = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
@@ -62,13 +61,21 @@ async function main() {
|
||||
try {
|
||||
const token =
|
||||
getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
|
||||
req.cookies['token'];
|
||||
req.user = await identity.authenticate(token);
|
||||
(req.cookies.token as string | undefined);
|
||||
if (!token) {
|
||||
res.status(401).send('Unauthorized');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
req.user = await identity.authenticate(token);
|
||||
} catch {
|
||||
await appEnv.tokenManager.authenticate(token);
|
||||
}
|
||||
if (!req.headers.authorization) {
|
||||
// Authorization header may be forwarded by plugin requests
|
||||
req.headers.authorization = `Bearer ${token}`;
|
||||
}
|
||||
if (token !== req.cookies['token']) {
|
||||
if (token && token !== req.cookies.token) {
|
||||
setTokenCookie(res, {
|
||||
token,
|
||||
secure,
|
||||
@@ -77,9 +84,24 @@ async function main() {
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).send(`Unauthorized`);
|
||||
res.status(401).send('Unauthorized');
|
||||
}
|
||||
};
|
||||
return authMiddleware;
|
||||
};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/index.ts from a create-app deployment
|
||||
|
||||
import { createAuthMiddleware } from './authMiddleware';
|
||||
|
||||
// ...
|
||||
|
||||
async function main() {
|
||||
// ...
|
||||
|
||||
const authMiddleware = await createAuthMiddleware(config, appEnv);
|
||||
|
||||
const apiRouter = Router();
|
||||
apiRouter.use(cookieParser());
|
||||
@@ -99,12 +121,10 @@ async function main() {
|
||||
}
|
||||
```
|
||||
|
||||
Create `packages/app/src/cookieAuth.ts`:
|
||||
|
||||
```typescript
|
||||
// packages/app/src/App.tsx from a create-app deployment
|
||||
|
||||
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
// ...
|
||||
import type { IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
// Parses supplied JWT token and returns the payload
|
||||
function parseJwt(token: string): { exp: number } {
|
||||
@@ -113,9 +133,11 @@ function parseJwt(token: string): { exp: number } {
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.map(
|
||||
c =>
|
||||
// eslint-disable-next-line prefer-template
|
||||
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2),
|
||||
)
|
||||
.join(''),
|
||||
);
|
||||
|
||||
@@ -132,7 +154,7 @@ function msUntilExpiry(token: string): number {
|
||||
|
||||
// Calls the specified url regularly using an auth token to set a token cookie
|
||||
// to authorize regular HTTP requests when loading techdocs
|
||||
async function setTokenCookie(url: string, identityApi: IdentityApi) {
|
||||
export async function setTokenCookie(url: string, identityApi: IdentityApi) {
|
||||
const { token } = await identityApi.getCredentials();
|
||||
if (!token) {
|
||||
return;
|
||||
@@ -155,6 +177,14 @@ async function setTokenCookie(url: string, identityApi: IdentityApi) {
|
||||
ms > 0 ? ms : 10000,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/app/src/App.tsx from a create-app deployment
|
||||
|
||||
import { setTokenCookie } from './cookieAuth';
|
||||
|
||||
// ...
|
||||
|
||||
const app = createApp({
|
||||
// ...
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
/// <reference types="cypress" />
|
||||
import 'os';
|
||||
|
||||
describe('score-card', () => {
|
||||
describe('Score board', () => {
|
||||
it('displays the score board based on sample data', () => {
|
||||
cy.loginAsGuest();
|
||||
|
||||
cy.visit('/score-board');
|
||||
cy.screenshot({ capture: 'viewport' });
|
||||
|
||||
cy.contains('System scores overview').should('be.visible');
|
||||
cy.checkForErrors();
|
||||
cy.get('span:contains("1-2 of 2")').should('be.visible'); // beware, there is also a hidden <P/> element
|
||||
cy.contains('audio-playback').should('be.visible');
|
||||
cy.contains('team-c').should('be.visible');
|
||||
cy.contains('non-valid-system').should('be.visible');
|
||||
cy.contains('Name').should('be.visible');
|
||||
cy.contains('Date').should('be.visible');
|
||||
cy.contains('Code').should('be.visible');
|
||||
cy.contains('Documentation').should('be.visible');
|
||||
cy.contains('Operations').should('be.visible');
|
||||
cy.contains('Quality').should('be.visible');
|
||||
cy.contains('Security').should('be.visible');
|
||||
cy.contains('Total').should('be.visible');
|
||||
cy.contains('50 %').should('be.visible');
|
||||
cy.contains('75 %').should('be.visible');
|
||||
cy.log('navigating to score card detail for audio-playback');
|
||||
cy.get('a[data-id="audio-playback"]').should('be.visible').click();
|
||||
cy.screenshot({ capture: 'viewport' });
|
||||
|
||||
cy.url().should(
|
||||
'include',
|
||||
'/catalog/default/System/audio-playback/score',
|
||||
);
|
||||
cy.contains('Scoring').should('be.visible');
|
||||
cy.contains('Total score: 57 %').should('be.visible');
|
||||
cy.contains('Code').should('be.visible');
|
||||
cy.contains('90 %').should('be.visible');
|
||||
cy.contains('Documentation').should('be.visible');
|
||||
cy.contains('75 %').should('be.visible');
|
||||
cy.contains('Operations').should('be.visible');
|
||||
cy.contains('50 %').should('be.visible');
|
||||
cy.contains('Quality').should('be.visible');
|
||||
cy.contains('25 %').should('be.visible');
|
||||
cy.contains('Security');
|
||||
cy.contains('10 %').should('be.visible');
|
||||
cy.checkForErrors();
|
||||
|
||||
cy.log(
|
||||
'Clicking on button [>] that is first child of the element (td) with value=Code',
|
||||
);
|
||||
cy.get('[value="Code"] > button:first-child').click();
|
||||
cy.checkForErrors();
|
||||
cy.screenshot({ capture: 'viewport' });
|
||||
|
||||
cy.log('Clicking on link for Code');
|
||||
cy.contains('hints: Gitflow: 100%').should('be.visible');
|
||||
cy.get('a[data-id="2157"]')
|
||||
.should('be.visible')
|
||||
.should(
|
||||
'have.attr',
|
||||
'href',
|
||||
'https://TBD/XXX/_wiki/wikis/XXX.wiki/2157',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -118,3 +118,8 @@ Cypress.Commands.add('waitSectionTwoPage', () => {
|
||||
Cypress.Commands.add('waitHomePage', () => {
|
||||
cy.wait(['@entityMetadata', '@syncEntity', '@techdocsMetadata', '@homeHTML']);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkForErrors', () => {
|
||||
// when an error occurs there is a <div> with an "alert" role attribute. This can change ofc => we shall add also some positive ("when error occurs") test
|
||||
cy.get('div[role="alert"]').should('not.exist');
|
||||
});
|
||||
|
||||
Vendored
+5
@@ -77,5 +77,10 @@ declare namespace Cypress {
|
||||
* @example cy.isNotInViewport
|
||||
*/
|
||||
isNotInViewport(element: string): Chainable<Element>;
|
||||
/**
|
||||
* Check if we have not caused error by our last action
|
||||
* @example cy.checkForErrors
|
||||
*/
|
||||
checkForErrors(): Chainable<Element>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ auth:
|
||||
clientSecret: ${AUTH_AUTH0_CLIENT_SECRET}
|
||||
domain: ${AUTH_AUTH0_DOMAIN_ID}
|
||||
audience: ${AUTH_AUTH0_AUDIENCE}
|
||||
connection: ${AUTH_AUTH0_CONNECTION}
|
||||
connectionScope: ${AUTH_AUTH0_CONNECTION_SCOPE}
|
||||
```
|
||||
|
||||
The Auth0 provider is a structure with three configuration keys:
|
||||
@@ -44,6 +46,12 @@ The Auth0 provider is a structure with three configuration keys:
|
||||
page
|
||||
- `domain`: The Application domain, found on the Auth0 Application page
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
- `audience`: The intended recipients of the token
|
||||
- `connection`: Social identity provider name. To check the available social connections, please visit [Auth0 Social Connections](https://marketplace.auth0.com/features/social-connections).
|
||||
- `connectionScope`: Additional scopes in the interactive token request. It should always be used in combination with the `connection` parameter
|
||||
|
||||
## Adding the provider to the Backstage frontend
|
||||
|
||||
To add the provider to the frontend, add the `auth0AuthApi` reference and
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
---
|
||||
id: oidc
|
||||
title: OIDC provider from scratch
|
||||
description: This section shows how to use an OIDC provider from scratch, same steps apply for custom providers.
|
||||
---
|
||||
|
||||
This section shows how to use an OIDC provider from scratch, same steps apply for custom
|
||||
providers. Please note these steps are for using a provider, not how to implement one,
|
||||
and Backstage recommends creating custom providers specific to the IDP, so we'll use a
|
||||
`azureOIDC` provider throughout this example, feel free to change any of those refs
|
||||
to your provider name.
|
||||
|
||||
## Summary
|
||||
|
||||
To add providers not enabled by default like OIDC, we need to follow some steps, we
|
||||
assume you already have a sign in page to which we'll add the provider so users can
|
||||
sign in through the provider. In simple steps here's how you enable the provider:
|
||||
|
||||
- Create an API reference to identify the provider.
|
||||
- Create the API factory that will handle the authentication.
|
||||
- Add or reuse an auth provider so you can authenticate.
|
||||
- Add or reuse a resolver to handle the result from the authentication.
|
||||
- Configure the provider to access your 3rd party auth solution.
|
||||
- Add the provider to sign in page so users can login with it.
|
||||
|
||||
We'll explain each step more in detail next.
|
||||
|
||||
### The API reference
|
||||
|
||||
An API reference exist for the sake of **Dependency Injection**, check [Utility APIs][4]
|
||||
for extended explanation.
|
||||
|
||||
In this OIDC example, we'll create the API reference directly in the
|
||||
`packages/app/src/apis.ts` file, it is not a requirement to put the reference in this
|
||||
file. Any location will do as long as it's available to be imported to where the API
|
||||
factory is, as well as easily accessible to the rest of the application so any package
|
||||
and plugin can inject the API instance when necessary.
|
||||
|
||||
An example of such would be when you use an auth provider from a library installed with
|
||||
NPM, or any other library repository, you would import the API ref from the library.
|
||||
|
||||
```ts
|
||||
export const azureOIDCAuthApiRef: ApiRef<
|
||||
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
> = createApiRef({
|
||||
id: 'auth.my-custom-provider',
|
||||
});
|
||||
```
|
||||
|
||||
Please note a few things, the ID can be anything you want as long as it doesn't conflict
|
||||
with other refs, backstage recommends to use a custom name that references your custom
|
||||
provider, for example we are using OIDC protocol with Azure, so we could use something
|
||||
like `auth.azure.oidc` as well.
|
||||
|
||||
Also we're exporting this reference, as well as the `typings`, we need to
|
||||
be able to import this reference anywhere in the app, and the `typings` will tell typescript
|
||||
what instance we're getting from DI when injecting the API. In this case we are defining
|
||||
an API for authentication, so we tell TS that this instance complies with 4 API
|
||||
interfaces:
|
||||
|
||||
- The OICD API that will handle authentication.
|
||||
- Profile API for requesting user profile info from the auth provider in question.
|
||||
- Backstage identity API to handle and associate the user profile with backstage identity.
|
||||
- Session API, to handle the session the user will have while logged in.
|
||||
|
||||
### The API Factory
|
||||
|
||||
A factory is a function that can take some parameters or dependencies and return an
|
||||
instance of something, in our case it will be a function that requests some backstage
|
||||
APIs and use them to create an instance of an OIDC API provider.
|
||||
|
||||
Please note that this function only runs (creates the instance) when somewhere else in
|
||||
the app you request the DI to give you an instance of the OIDC provider using the API ref
|
||||
defined above, and the DI will only run this function the first time, from then on any
|
||||
other DI injection will just receive the same instance created the first time, basically
|
||||
the instance is cached by the DI library, a singleton.
|
||||
|
||||
Let's add our OIDC API factory to the APIs array in the `packages/app/src/apis.ts` file:
|
||||
|
||||
```diff
|
||||
+ import { OAuth2 } from '@backstage/core-app-api';
|
||||
|
||||
export const apis: AnyApiFactory[] = [
|
||||
+ createApiFactory({
|
||||
+ api: azureOIDCAuthApiRef,
|
||||
+ deps: {
|
||||
+ discoveryApi: discoveryApiRef,
|
||||
+ oauthRequestApi: oauthRequestApiRef,
|
||||
+ configApi: configApiRef,
|
||||
+ },
|
||||
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
|
||||
+ OAuth2.create({
|
||||
+ discoveryApi,
|
||||
+ oauthRequestApi,
|
||||
+ provider: {
|
||||
+ id: 'my-auth-provider',
|
||||
+ title: 'My custom auth provider',
|
||||
+ icon: () => null,
|
||||
+ },
|
||||
+ environment: configApi.getOptionalString('auth.environment'),
|
||||
+ defaultScopes: [
|
||||
+ 'openid',
|
||||
+ 'profile',
|
||||
+ 'email',
|
||||
+ ],
|
||||
+ }),
|
||||
+ }),
|
||||
|
||||
```
|
||||
|
||||
Please note we're importing the `OAuth2` class from `@backstage/core-app-api` effectively
|
||||
delegating the authentication to it. Also we're using the `my-auth-provider` ID to tell
|
||||
`OAuth2` to use the auth provider we'll define in the next section, and added the default
|
||||
scopes to request ID, profile, email and user read permissions.
|
||||
|
||||
## The Auth Provider
|
||||
|
||||
The Auth Provider is responsible for authenticating with the 3rd party service, and give
|
||||
us back the credentials, here's where you pick which protocol to use, be it Auth0, OAuth2,
|
||||
OIDC, SAML or any other that your 3rd party IDP provider supports.
|
||||
|
||||
For this example we'll use OIDC, we pass a factory to the `providerFactories` object with
|
||||
the ID you picked to represent the Auth provider, this ID has to match with the provider's
|
||||
`id` inside the API factory, the yaml config provider key under `auth.providers`, and the
|
||||
callback URI provider segment (you'll have to configure your IDP to handle the callback
|
||||
URI properly).
|
||||
|
||||
```diff
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
database: env.database,
|
||||
discovery: env.discovery,
|
||||
tokenManager: env.tokenManager,
|
||||
providerFactories: {
|
||||
...defaultAuthProviderFactories,
|
||||
+ 'my-auth-provider': providers.oidc.create({
|
||||
+ }),
|
||||
}
|
||||
```
|
||||
|
||||
### The Resolver
|
||||
|
||||
Resolvers exist to map user identity from the 3rd party (in this case an azure IDP
|
||||
provider) to the backstage user identity, for a detailed explanation check the
|
||||
[Identity Resolver][1] page, it explains how to write a custom resolver as well as
|
||||
linking the built in resolvers of backstage.
|
||||
|
||||
The default OIDC provider does not support SignIn, we need to add such support by
|
||||
adding a resolver for a SignIn request.
|
||||
|
||||
The OIDC provider doesn't provide any build-in resolvers, so we'll need to define our own:
|
||||
|
||||
```diff
|
||||
import {
|
||||
DEFAULT_NAMESPACE,
|
||||
+ stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
database: env.database,
|
||||
discovery: env.discovery,
|
||||
tokenManager: env.tokenManager,
|
||||
providerFactories: {
|
||||
...defaultAuthProviderFactories,
|
||||
'my-auth-provider': providers.oidc.create({
|
||||
+ signIn: {
|
||||
+ resolver(info, ctx) {
|
||||
+ const userRef = stringifyEntityRef({
|
||||
+ kind: 'User',
|
||||
+ name: info.result.userinfo.sub,
|
||||
+ namespace: DEFAULT_NAMESPACE,
|
||||
+ });
|
||||
+ return ctx.issueToken({
|
||||
+ claims: {
|
||||
+ sub: userRef, // The user's own identity
|
||||
+ ent: [userRef], // A list of identities that the user claims ownership through
|
||||
+ },
|
||||
+ });
|
||||
+ },
|
||||
+ },
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
### The configuration
|
||||
|
||||
Since we are using our custom OIDC Auth Provider, we need to add a configuration based
|
||||
on the provider used, in this case based on OIDC protocol (remember the 3rd party has to
|
||||
support the protocol).
|
||||
|
||||
In this example we'll configure OIDC with `my-auth-provider`, to do so we need to
|
||||
[Create app registration][2] in the Azure console, the only difference is that the
|
||||
`http://localhost:7007/api/auth/microsoft/handler/frame` URL needs to change to
|
||||
`http://localhost:7007/api/auth/my-auth-provider/handler/frame`.
|
||||
|
||||
Then we need to configure the env variables for the provider, based on the provider's code
|
||||
in `plugins/auth-backend/src/providers/oidc/provider.ts` we need the following variables
|
||||
in the `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
environment: development
|
||||
### Providing an auth.session.secret will enable session support in the auth-backend
|
||||
session:
|
||||
secret: ${SESSION_SECRET}
|
||||
providers:
|
||||
my-auth-provider:
|
||||
development:
|
||||
metadataUrl: https://example.com/.well-known/openid-configuration
|
||||
clientId: ${AUTH_MY_CLIENT_ID}
|
||||
clientSecret: ${AUTH_MY_CLIENT_SECRET}
|
||||
```
|
||||
|
||||
Anything enclosed in `${}` can be replaced directly in the yaml, or provided as
|
||||
environment variables, the way you obtain all these except `scope` and `prompt` is to
|
||||
check the App Registration you created:
|
||||
|
||||
- `clientId`: Grab from the Overview page.
|
||||
- `clientSecret`: Can only be seen when creating the secret, if you lose it you'll need a
|
||||
new secret.
|
||||
- `metadataUrl`: In Overview > Endpoints tab, grab OpenID Connect metadata document URL.
|
||||
- `authorizationUrl` and `tokenUrl`: Open the `metadataUrl` in a browser, that json will
|
||||
hold these 2 urls somewhere in there.
|
||||
- `tokenSignedResponseAlg`: Don't define it, use the default unless you know what it does.
|
||||
- `scope`: Only used if we didn't specify `defaultScopes` in the provider's factory,
|
||||
basically the same thing.
|
||||
- `prompt`: Recommended to use `auto` so the browser will request login to the IDP if the
|
||||
user has no active session.
|
||||
|
||||
Note that for the time being, any change in this yaml file requires a restart of the app,
|
||||
also you need to have the `session.secret` part to use OIDC (some other providers might
|
||||
need this as well) to support user sessions.
|
||||
|
||||
### The Sign In provider
|
||||
|
||||
The last step is to add the provider to the `SignInPage` so users can sign in with your
|
||||
new provider, please follow the [Sing In Configuration][3] docs, here's where you import
|
||||
and use the API ref we defined earlier.
|
||||
|
||||
## Note
|
||||
|
||||
These steps apply to most if not all the providers, including custom providers, the main
|
||||
difference between different providers will be the contents of the API factory, the code
|
||||
in the Auth Provider Factory, the resolver, and the different variables each provider
|
||||
needs in the YAML config or env variables.
|
||||
|
||||
[1]: https://backstage.io/docs/auth/identity-resolver
|
||||
[2]: https://backstage.io/docs/auth/microsoft/provider#create-an-app-registration-on-azure
|
||||
[3]: https://backstage.io/docs/auth/#sign-in-configuration
|
||||
[4]: https://backstage.io/docs/api/utility-apis
|
||||
@@ -101,7 +101,7 @@ Similarly to Lunr above, ElasticSearch can be set up like this:
|
||||
|
||||
```typescript
|
||||
// app/backend/src/plugins/search.ts
|
||||
const searchEngine = await ElasticSearchSearchEngine.initialize({
|
||||
const searchEngine = await ElasticSearchSearchEngine.fromConfig({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
});
|
||||
|
||||
@@ -14,8 +14,7 @@ appropriate source code repository for your organization.
|
||||
|
||||
> Note: Integrations may already be set up as part of your `app-config.yaml`.
|
||||
|
||||
The next step is to add
|
||||
[add templates](http://backstage.io/docs/features/software-templates/adding-templates)
|
||||
The next step is to [add templates](http://backstage.io/docs/features/software-templates/adding-templates)
|
||||
to your Backstage app.
|
||||
|
||||
## Publishing defaults
|
||||
|
||||
@@ -123,7 +123,7 @@ will set the available actions that the scaffolder has access to.
|
||||
```ts
|
||||
import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { createNewFileAction } from './actions/custom';
|
||||
import { createNewFileAction } from './scaffolder/actions/custom';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
|
||||
@@ -37,6 +37,7 @@ And then add the entity provider to your catalog builder:
|
||||
+ builder.addEntityProvider(
|
||||
+ BitbucketCloudEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ // optional: alternatively, configure via app-config.yaml
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { minutes: 30 },
|
||||
+ timeout: { minutes: 3 },
|
||||
@@ -67,24 +68,38 @@ catalog:
|
||||
filters: # optional
|
||||
projectKey: '^apis-.*$' # optional; RegExp
|
||||
repoSlug: '^service-.*$' # optional; RegExp
|
||||
schedule: # optional; same options as in TaskScheduleDefinition
|
||||
# supports cron, ISO duration, "human duration" as used in code
|
||||
frequency: { minutes: 30 }
|
||||
# supports ISO duration, "human duration" as used in code
|
||||
timeout: { minutes: 3 }
|
||||
workspace: workspace-name
|
||||
```
|
||||
|
||||
> **Note:** It is possible but certainly not recommended to skip the provider ID level.
|
||||
> If you do so, `default` will be used as provider ID.
|
||||
|
||||
- **catalogPath** _(optional)_:
|
||||
- **`catalogPath`** _(optional)_:
|
||||
Default: `/catalog-info.yaml`.
|
||||
Path where to look for `catalog-info.yaml` files.
|
||||
When started with `/`, it is an absolute path from the repo root.
|
||||
It supports values as allowed by the `path` filter/modifier
|
||||
[at Bitbucket Cloud's code search](https://confluence.atlassian.com/bitbucket/code-search-in-bitbucket-873876782.html#Search-Pathmodifier).
|
||||
- **filters** _(optional)_:
|
||||
- **projectKey** _(optional)_:
|
||||
- **`filters`** _(optional)_:
|
||||
- **`projectKey`** _(optional)_:
|
||||
Regular expression used to filter results based on the project key.
|
||||
- **repoSlug** _(optional)_:
|
||||
- **`repoSlug`** _(optional)_:
|
||||
Regular expression used to filter results based on the repo slug.
|
||||
- **workspace**:
|
||||
- **`schedule`** _(optional)_:
|
||||
- **`frequency`**:
|
||||
How often you want the task to run. The system does its best to avoid overlapping invocations.
|
||||
- **`timeout`**:
|
||||
The maximum amount of time that a single task invocation can take.
|
||||
- **`initialDelay`** _(optional)_:
|
||||
The amount of time that should pass before the first invocation happens.
|
||||
- **`scope`** _(optional)_:
|
||||
`'global'` or `'local'`. Sets the scope of concurrency control.
|
||||
- **`workspace`**:
|
||||
Name of your organization account/workspace.
|
||||
If you want to add multiple workspaces, you need to add one provider config each.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ The GitHub integration has a discovery provider for discovering catalog
|
||||
entities within a GitHub organization. The provider will crawl the GitHub
|
||||
organization and register entities matching the configured path. This can be
|
||||
useful as an alternative to static locations or manually adding things to the
|
||||
catalog. This is the prefered method for ingesting entities into the catalog.
|
||||
catalog. This is the preferred method for ingesting entities into the catalog.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -55,7 +55,7 @@ And then add the entity provider to your catalog builder:
|
||||
To use the discovery provider, you'll need a GitHub integration
|
||||
[set up](locations.md) with either a [Personal Access Token](../../getting-started/configuration.md#setting-up-a-github-integration) or [GitHub Apps](./github-apps.md).
|
||||
|
||||
Then you can add a github config to the catalog providers configuration:
|
||||
Then you can add a `github` config to the catalog providers configuration:
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
@@ -96,6 +96,10 @@ catalog:
|
||||
topic:
|
||||
include: ['backstage-include'] # optional array of strings
|
||||
exclude: ['experiments'] # optional array of strings
|
||||
enterpriseProviderId:
|
||||
host: ghe.example.net
|
||||
organization: 'backstage' # string
|
||||
catalogPath: '/catalog-info.yaml' # string
|
||||
```
|
||||
|
||||
This provider supports multiple organizations via unique provider IDs.
|
||||
@@ -117,14 +121,16 @@ This provider supports multiple organizations via unique provider IDs.
|
||||
In the example above, a repository with the `backstage-include` topic would still be excluded
|
||||
if it were also carrying the `experiments` topic.
|
||||
- **include** _(optional)_:
|
||||
An array of strings used to filter in results based on their associated Github topics.
|
||||
An array of strings used to filter in results based on their associated GitHub topics.
|
||||
If configured, only repositories with one (or more) topic(s) present in the inclusion filter will be ingested
|
||||
- **exclude** _(optional)_:
|
||||
An array of strings used to filter out results based on their associated Github topics.
|
||||
An array of strings used to filter out results based on their associated GitHub topics.
|
||||
If configured, all repositories _except_ those with one (or more) topics(s) present in the exclusion filter will be ingested.
|
||||
- **organization**:
|
||||
Name of your organization account/workspace.
|
||||
If you want to add multiple organizations, you need to add one provider config each.
|
||||
- **host** _(optional)_:
|
||||
The hostname of your GitHub Enterprise instance. It must match a host defined in [integrations.github](locations.md).
|
||||
|
||||
## GitHub API Rate Limits
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ description: Support and Community Details and Links
|
||||
|
||||
- [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the
|
||||
project.
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/backstage) - Browse or ask questions on Stack Overflow.
|
||||
- [Good First Issues](https://github.com/backstage/backstage/contribute) - Start
|
||||
here if you want to contribute.
|
||||
- [RFCs](https://github.com/backstage/backstage/labels/rfc) - Help shape the
|
||||
|
||||
@@ -132,7 +132,7 @@ router.use('/summary', async (req, res) => {
|
||||
]).then(async ([frobs, flerps, thunk]) => {
|
||||
return computeAggregate(await frobs.json(), await flerps.json(), thunk);
|
||||
});
|
||||
res.status(200).send(agg);
|
||||
res.status(200).json(agg);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,13 @@ COPY .yarn ./.yarn
|
||||
COPY .yarnrc.yml ./
|
||||
```
|
||||
|
||||
In a multi-stage `Dockerfile`, each stage that runs a `yarn` command will also need the Yarn 3 installation. For example, in the final stage you may need to add the following:
|
||||
|
||||
```Dockerfile
|
||||
COPY --from=build --chown=node:node /app/.yarn ./.yarn
|
||||
COPY --from=build --chown=node:node /app/.yarnrc.yml ./
|
||||
```
|
||||
|
||||
The `--production` flag to `yarn install` has been removed in Yarn 3, instead you need to use `yarn workspaces focus --all --production` to avoid installing development dependencies in your production deployment. A tradeoff of this is that `yarn workspaces focus` does not support the `--immutable` flag.
|
||||
|
||||
```Dockerfile
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
---
|
||||
title: Adopters Community Sessions
|
||||
date: August 17, 2022
|
||||
category: Upcoming
|
||||
category: Meetup
|
||||
description: Adopters Community Session ✨. It's the monthly meetup where we all come together to listen to the latest maintainer updates, learn from each other about adopting, share exciting new demos or discuss any relevant topic like developer effectiveness, developer experience, developer portals, etc.
|
||||
youtubeUrl: https://youtu.be/qYnvc8ge1kg
|
||||
youtubeImgUrl: https://backstage.io/img/b-sessions.png
|
||||
rsvpUrl: https://calendar.google.com/calendar/embed?src=c_qup9gbhn9sqpuao6trttd8mk5s@group.calendar.google.com
|
||||
eventUrl: https://github.com/backstage/community/issues/54
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
---
|
||||
title: Contributor Community Sessions
|
||||
date: August 24, 2022
|
||||
category: Upcoming
|
||||
category: Meetup
|
||||
description: Join the maintainers and contributors for the Contributor Community Sessions
|
||||
youtubeUrl: https://youtu.be/qYnvc8ge1kg
|
||||
youtubeUrl: https://youtu.be/8ydEFFiuHAc
|
||||
youtubeImgUrl: https://backstage.io/img/b-sessions.png
|
||||
rsvpUrl: https://calendar.google.com/calendar/embed?src=c_qup9gbhn9sqpuao6trttd8mk5s@group.calendar.google.com
|
||||
eventUrl: https://github.com/backstage/community/issues/54
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Adopters Community Sessions
|
||||
date: September 21, 2022
|
||||
category: Meetup
|
||||
description: Adopters Community Session ✨. It's the monthly meetup where we all come together to listen to the latest maintainer updates, learn from each other about adopting, share exciting new demos or discuss any relevant topic like developer effectiveness, developer experience, developer portals, etc.
|
||||
youtubeUrl: https://youtu.be/K44RQAVWWnY
|
||||
youtubeImgUrl: https://backstage.io/img/b-sessions.png
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Contributor Community Sessions
|
||||
date: September 28, 2022
|
||||
category: Meetup
|
||||
description: Join the maintainers and contributors for the Contributor Community Sessions
|
||||
youtubeUrl: https://youtu.be/jmNT5x3mKaQ
|
||||
youtubeImgUrl: https://backstage.io/img/b-sessions.png
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: System scoring
|
||||
author: Oriflame
|
||||
authorUrl: https://github.com/Oriflame
|
||||
category: Quality
|
||||
description: Visualization of maturity (and improving it) of services/systems thanks to a review process.
|
||||
documentation: https://github.com/Oriflame/backstage-plugins/tree/main/plugins/score-card
|
||||
iconUrl: img/score-card-plugin-logo.png
|
||||
npmPackageName: '@oriflame/backstage-plugin-score-card'
|
||||
@@ -278,6 +278,7 @@
|
||||
},
|
||||
"auth/identity-resolver",
|
||||
"auth/oauth",
|
||||
"auth/oidc",
|
||||
"auth/add-auth-provider",
|
||||
"auth/service-to-service-auth",
|
||||
"auth/troubleshooting",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -163,6 +163,7 @@ nav:
|
||||
- Bitbucket: 'auth/bitbucket/provider.md'
|
||||
- Sign in resolvers: 'auth/identity-resolver.md'
|
||||
- OAuth and OpenID Connect: 'auth/oauth.md'
|
||||
- OIDC provider from scratch: 'auth/oidc.md'
|
||||
- Contributing New Providers: 'auth/add-auth-provider.md'
|
||||
- Service to Service Auth: 'auth/service-to-service-auth.md'
|
||||
- Troubleshooting Auth: 'auth/troubleshooting.md'
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@
|
||||
"@types/react": "^17",
|
||||
"@types/react-dom": "^17"
|
||||
},
|
||||
"version": "1.7.0-next.0",
|
||||
"version": "1.7.0-next.1",
|
||||
"dependencies": {
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@microsoft/api-documenter": "^7.17.11",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @backstage/app-defaults
|
||||
|
||||
## 1.0.7-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/core-app-api@1.1.1-next.1
|
||||
- @backstage/core-components@0.11.2-next.1
|
||||
- @backstage/core-plugin-api@1.0.7-next.1
|
||||
- @backstage/theme@0.2.16
|
||||
- @backstage/plugin-permission-react@0.4.6-next.1
|
||||
|
||||
## 1.0.7-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/app-defaults",
|
||||
"description": "Provides the default wiring of a Backstage App",
|
||||
"version": "1.0.7-next.0",
|
||||
"version": "1.0.7-next.1",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
|
||||
@@ -1,5 +1,67 @@
|
||||
# example-app
|
||||
|
||||
## 0.2.76-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-user-settings@0.5.0-next.1
|
||||
- @backstage/plugin-scaffolder@1.7.0-next.1
|
||||
- @backstage/plugin-catalog-react@1.2.0-next.1
|
||||
- @backstage/core-app-api@1.1.1-next.1
|
||||
- @backstage/cli@0.20.0-next.1
|
||||
- @backstage/plugin-tech-insights@0.3.1-next.1
|
||||
- @backstage/plugin-gcalendar@0.3.6-next.1
|
||||
- @backstage/plugin-api-docs@0.8.10-next.1
|
||||
- @backstage/plugin-search-react@1.2.0-next.1
|
||||
- @backstage/plugin-search-common@1.1.0-next.1
|
||||
- @backstage/plugin-airbrake@0.3.10-next.1
|
||||
- @backstage/plugin-azure-devops@0.2.1-next.1
|
||||
- @backstage/plugin-badges@0.2.34-next.1
|
||||
- @internal/plugin-catalog-customized@0.0.3-next.1
|
||||
- @backstage/plugin-catalog-graph@0.2.22-next.1
|
||||
- @backstage/plugin-catalog-import@0.8.13-next.1
|
||||
- @backstage/plugin-circleci@0.3.10-next.1
|
||||
- @backstage/plugin-cloudbuild@0.3.10-next.1
|
||||
- @backstage/plugin-code-coverage@0.2.3-next.1
|
||||
- @backstage/plugin-dynatrace@1.0.0-next.1
|
||||
- @backstage/plugin-explore@0.3.41-next.1
|
||||
- @backstage/plugin-github-actions@0.5.10-next.1
|
||||
- @backstage/plugin-gocd@0.1.16-next.1
|
||||
- @backstage/plugin-home@0.4.26-next.1
|
||||
- @backstage/plugin-jenkins@0.7.9-next.1
|
||||
- @backstage/plugin-kafka@0.3.10-next.1
|
||||
- @backstage/plugin-kubernetes@0.7.3-next.1
|
||||
- @backstage/plugin-lighthouse@0.3.10-next.1
|
||||
- @backstage/plugin-newrelic-dashboard@0.2.3-next.1
|
||||
- @backstage/plugin-org@0.5.10-next.1
|
||||
- @backstage/plugin-pagerduty@0.5.3-next.1
|
||||
- @backstage/plugin-playlist@0.1.1-next.1
|
||||
- @backstage/plugin-rollbar@0.4.10-next.1
|
||||
- @backstage/plugin-search@1.0.3-next.1
|
||||
- @backstage/plugin-sentry@0.4.3-next.1
|
||||
- @backstage/plugin-techdocs@1.3.3-next.1
|
||||
- @backstage/plugin-todo@0.2.12-next.1
|
||||
- @backstage/app-defaults@1.0.7-next.1
|
||||
- @backstage/core-components@0.11.2-next.1
|
||||
- @backstage/core-plugin-api@1.0.7-next.1
|
||||
- @backstage/plugin-apache-airflow@0.2.3-next.1
|
||||
- @backstage/plugin-cost-insights@0.11.32-next.1
|
||||
- @backstage/plugin-gcp-projects@0.3.29-next.1
|
||||
- @backstage/plugin-graphiql@0.2.42-next.1
|
||||
- @backstage/plugin-newrelic@0.3.28-next.1
|
||||
- @backstage/plugin-shortcuts@0.3.2-next.1
|
||||
- @backstage/plugin-stack-overflow@0.1.6-next.1
|
||||
- @backstage/plugin-tech-radar@0.5.17-next.1
|
||||
- @backstage/plugin-techdocs-module-addons-contrib@1.0.5-next.1
|
||||
- @backstage/catalog-model@1.1.2-next.1
|
||||
- @backstage/config@1.0.3-next.1
|
||||
- @backstage/integration-react@1.1.5-next.1
|
||||
- @backstage/theme@0.2.16
|
||||
- @backstage/plugin-catalog-common@1.0.7-next.1
|
||||
- @backstage/plugin-permission-react@0.4.6-next.1
|
||||
- @backstage/plugin-techdocs-react@1.0.5-next.1
|
||||
|
||||
## 0.2.76-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.2.76-next.0",
|
||||
"version": "0.2.76-next.1",
|
||||
"private": true,
|
||||
"backstage": {
|
||||
"role": "frontend"
|
||||
@@ -67,6 +67,7 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"@octokit/rest": "^19.0.3",
|
||||
"@oriflame/backstage-plugin-score-card": "^0.5.1",
|
||||
"@roadiehq/backstage-plugin-buildkite": "^2.0.8",
|
||||
"@roadiehq/backstage-plugin-github-insights": "^2.0.5",
|
||||
"@roadiehq/backstage-plugin-github-pull-requests": "^2.2.7",
|
||||
|
||||
@@ -106,6 +106,7 @@ import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
|
||||
import { PlaylistIndexPage } from '@backstage/plugin-playlist';
|
||||
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
|
||||
import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
@@ -273,6 +274,7 @@ const routes = (
|
||||
<Route path="/azure-pull-requests" element={<AzurePullRequestsPage />} />
|
||||
<Route path="/apache-airflow" element={<ApacheAirflowPage />} />
|
||||
<Route path="/playlist" element={<PlaylistIndexPage />} />
|
||||
<Route path="/score-board" element={<ScoreBoardPage />} />
|
||||
</FlatRoutes>
|
||||
);
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
import { MyGroupsSidebarItem } from '@backstage/plugin-org';
|
||||
import GroupIcon from '@material-ui/icons/People';
|
||||
import { SearchModal } from '../search/SearchModal';
|
||||
import Score from '@material-ui/icons/Score';
|
||||
|
||||
const useSidebarLogoStyles = makeStyles({
|
||||
root: {
|
||||
@@ -120,6 +121,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
text="Cost Insights"
|
||||
/>
|
||||
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
|
||||
<SidebarItem icon={Score} to="score-board" text="Score board" />
|
||||
</SidebarScrollWrapper>
|
||||
<SidebarDivider />
|
||||
<Shortcuts />
|
||||
|
||||
@@ -144,6 +144,7 @@ import {
|
||||
EntityNewRelicDashboardCard,
|
||||
} from '@backstage/plugin-newrelic-dashboard';
|
||||
import { EntityGoCdContent, isGoCdAvailable } from '@backstage/plugin-gocd';
|
||||
import { EntityScoreCardContent } from '@oriflame/backstage-plugin-score-card';
|
||||
|
||||
import React, { ReactNode, useMemo, useState } from 'react';
|
||||
|
||||
@@ -704,6 +705,13 @@ const systemPage = (
|
||||
</Grid>
|
||||
</Grid>
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route path="/score" title="Score">
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item xs={12}>
|
||||
<EntityScoreCardContent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route path="/diagram" title="Diagram">
|
||||
<EntityCatalogGraphCard
|
||||
variant="gridItem"
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @backstage/backend-app-api
|
||||
|
||||
## 0.2.2-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/backend-common@0.15.2-next.1
|
||||
- @backstage/backend-plugin-api@0.1.3-next.1
|
||||
- @backstage/backend-tasks@0.3.6-next.1
|
||||
- @backstage/errors@1.1.2-next.1
|
||||
- @backstage/plugin-permission-node@0.6.6-next.1
|
||||
|
||||
## 0.2.2-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-app-api",
|
||||
"description": "Core API used by Backstage backend apps",
|
||||
"version": "0.2.2-next.0",
|
||||
"version": "0.2.2-next.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @backstage/backend-common
|
||||
|
||||
## 0.15.2-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c31f7cdfbc: Fixed an issue where `getClient()` for a `pluginId` would return different clients and not share them
|
||||
- Updated dependencies
|
||||
- @backstage/cli-common@0.1.10
|
||||
- @backstage/config@1.0.3-next.1
|
||||
- @backstage/config-loader@1.1.5-next.1
|
||||
- @backstage/errors@1.1.2-next.1
|
||||
- @backstage/integration@1.3.2-next.1
|
||||
- @backstage/types@1.0.0
|
||||
|
||||
## 0.15.2-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-common",
|
||||
"description": "Common functionality library for Backstage backends",
|
||||
"version": "0.15.2-next.0",
|
||||
"version": "0.15.2-next.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -113,7 +113,11 @@ describe('DatabaseManager', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const manager = DatabaseManager.fromConfig(new ConfigReader(config));
|
||||
let manager: DatabaseManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = DatabaseManager.fromConfig(new ConfigReader(config));
|
||||
});
|
||||
|
||||
it('connects to a plugin database using default config', async () => {
|
||||
const pluginId = 'pluginwithoutconfig';
|
||||
@@ -340,6 +344,16 @@ describe('DatabaseManager', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the same client for the same pluginId', async () => {
|
||||
const [client1, client2] = await Promise.all([
|
||||
manager.forPlugin('plugin1').getClient(),
|
||||
manager.forPlugin('plugin1').getClient(),
|
||||
]);
|
||||
expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(client1).toBe(client2);
|
||||
});
|
||||
|
||||
it('uses plugin connection as base if default client is different from plugin client', async () => {
|
||||
const pluginId = 'differentclient';
|
||||
await manager.forPlugin(pluginId).getClient();
|
||||
|
||||
@@ -84,6 +84,7 @@ export class DatabaseManager {
|
||||
private readonly config: Config,
|
||||
private readonly prefix: string = 'backstage_plugin_',
|
||||
private readonly options?: DatabaseManagerOptions,
|
||||
private readonly databaseCache: Map<string, Promise<Knex>> = new Map(),
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -307,45 +308,57 @@ export class DatabaseManager {
|
||||
* plugin
|
||||
*/
|
||||
private async getDatabase(pluginId: string): Promise<Knex> {
|
||||
const pluginConfig = new ConfigReader(
|
||||
this.getConfigForPlugin(pluginId) as JsonObject,
|
||||
);
|
||||
|
||||
const databaseName = this.getDatabaseName(pluginId);
|
||||
if (databaseName && this.getEnsureExistsConfig(pluginId)) {
|
||||
try {
|
||||
await ensureDatabaseExists(pluginConfig, databaseName);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`,
|
||||
);
|
||||
}
|
||||
if (this.databaseCache.has(pluginId)) {
|
||||
return this.databaseCache.get(pluginId)!;
|
||||
}
|
||||
|
||||
let schemaOverrides;
|
||||
if (this.getPluginDivisionModeConfig() === 'schema') {
|
||||
schemaOverrides = this.getSchemaOverrides(pluginId);
|
||||
if (this.getEnsureExistsConfig(pluginId)) {
|
||||
const clientPromise = Promise.resolve().then(async () => {
|
||||
const pluginConfig = new ConfigReader(
|
||||
this.getConfigForPlugin(pluginId) as JsonObject,
|
||||
);
|
||||
|
||||
const databaseName = this.getDatabaseName(pluginId);
|
||||
if (databaseName && this.getEnsureExistsConfig(pluginId)) {
|
||||
try {
|
||||
await ensureSchemaExists(pluginConfig, pluginId);
|
||||
await ensureDatabaseExists(pluginConfig, databaseName);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`,
|
||||
`Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const databaseClientOverrides = mergeDatabaseConfig(
|
||||
{},
|
||||
this.getDatabaseOverrides(pluginId),
|
||||
schemaOverrides,
|
||||
);
|
||||
let schemaOverrides;
|
||||
if (this.getPluginDivisionModeConfig() === 'schema') {
|
||||
schemaOverrides = this.getSchemaOverrides(pluginId);
|
||||
if (this.getEnsureExistsConfig(pluginId)) {
|
||||
try {
|
||||
await ensureSchemaExists(pluginConfig, pluginId);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const client = createDatabaseClient(pluginConfig, databaseClientOverrides);
|
||||
this.startKeepaliveLoop(pluginId, client);
|
||||
const databaseClientOverrides = mergeDatabaseConfig(
|
||||
{},
|
||||
this.getDatabaseOverrides(pluginId),
|
||||
schemaOverrides,
|
||||
);
|
||||
|
||||
return client;
|
||||
const client = createDatabaseClient(
|
||||
pluginConfig,
|
||||
databaseClientOverrides,
|
||||
);
|
||||
this.startKeepaliveLoop(pluginId, client);
|
||||
return client;
|
||||
});
|
||||
|
||||
this.databaseCache.set(pluginId, clientPromise);
|
||||
|
||||
return clientPromise;
|
||||
}
|
||||
|
||||
private startKeepaliveLoop(pluginId: string, client: Knex): void {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { notFoundHandler } from './notFoundHandler';
|
||||
describe('notFoundHandler', () => {
|
||||
it('handles only missing routes', async () => {
|
||||
const app = express();
|
||||
app.use('/exists', (_, res) => res.status(200).send());
|
||||
app.use('/exists', (_, res) => res.status(200).end());
|
||||
app.use(notFoundHandler());
|
||||
|
||||
const existsResponse = await request(app).get('/exists');
|
||||
|
||||
@@ -28,6 +28,6 @@ import { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||
export function notFoundHandler(): RequestHandler {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
return (_request: Request, response: Response, _next: NextFunction) => {
|
||||
response.status(404).send();
|
||||
response.status(404).end();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ describe('requestLoggingHandler', () => {
|
||||
|
||||
const app = express();
|
||||
app.use(requestLoggingHandler(logger));
|
||||
app.use('/exists1', (_, res) => res.status(200).send());
|
||||
app.use('/exists2', (_, res) => res.status(201).send());
|
||||
app.use('/exists1', (_, res) => res.status(200).end());
|
||||
app.use('/exists2', (_, res) => res.status(201).end());
|
||||
|
||||
const r = request(app);
|
||||
await r.get('/exists1');
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function statusCheckHandler(
|
||||
return async (_request: Request, response: Response, next: NextFunction) => {
|
||||
try {
|
||||
const status = await statusCheck();
|
||||
response.status(200).header('').send(status);
|
||||
response.status(200).json(status);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
@@ -83,11 +83,23 @@ describe('BitbucketServerUrlReader', () => {
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits/*',
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }),
|
||||
ctx.json({
|
||||
size: 2,
|
||||
values: [
|
||||
{
|
||||
displayId: 'some-branch-that-should-be-ignored',
|
||||
latestCommit: 'bogus hash',
|
||||
},
|
||||
{
|
||||
displayId: 'some-branch',
|
||||
latestCommit: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -130,12 +142,22 @@ describe('BitbucketServerUrlReader', () => {
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits/*',
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
size: 2,
|
||||
values: [
|
||||
{
|
||||
displayId: 'some-branch-that-should-be-ignored',
|
||||
latestCommit: 'bogus hash',
|
||||
},
|
||||
{
|
||||
displayId: 'some-branch',
|
||||
latestCommit: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -179,11 +201,23 @@ describe('BitbucketServerUrlReader', () => {
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits/*',
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }),
|
||||
ctx.json({
|
||||
size: 2,
|
||||
values: [
|
||||
{
|
||||
displayId: 'master-of-none',
|
||||
latestCommit: 'bogus hash',
|
||||
},
|
||||
{
|
||||
displayId: 'master',
|
||||
latestCommit: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -188,38 +188,36 @@ export class BitbucketServerUrlReader implements UrlReader {
|
||||
private async getLastCommitShortHash(url: string): Promise<string> {
|
||||
const { name: repoName, owner: project, ref: branch } = parseGitUrl(url);
|
||||
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp224
|
||||
const commitApiUrl = `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits/${branch}`;
|
||||
const branchListUrl = `${
|
||||
this.integration.config.apiBaseUrl
|
||||
}/projects/${project}/repos/${repoName}/branches?filterText=${encodeURIComponent(
|
||||
branch,
|
||||
)}`;
|
||||
|
||||
const commitResponse = await fetch(
|
||||
commitApiUrl,
|
||||
const branchListResponse = await fetch(
|
||||
branchListUrl,
|
||||
getBitbucketServerRequestOptions(this.integration.config),
|
||||
);
|
||||
if (!commitResponse.ok) {
|
||||
const message = `Failed to retrieve commits from ${commitApiUrl}, ${commitResponse.status} ${commitResponse.statusText}`;
|
||||
if (commitResponse.status === 404) {
|
||||
if (!branchListResponse.ok) {
|
||||
const message = `Failed to retrieve branch list from ${branchListUrl}, ${branchListResponse.status} ${branchListResponse.statusText}`;
|
||||
if (branchListResponse.status === 404) {
|
||||
throw new NotFoundError(message);
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const commits = await commitResponse.json();
|
||||
const branchMatches = await branchListResponse.json();
|
||||
|
||||
// Handles case when a branch is provided in the URL
|
||||
if (commits && commits.id) {
|
||||
return commits.id.substring(0, 12);
|
||||
if (branchMatches && branchMatches.size > 0) {
|
||||
const exactBranchMatch = branchMatches.values.filter(
|
||||
(branchDetails: { displayId: string }) =>
|
||||
branchDetails.displayId === branch,
|
||||
)[0];
|
||||
return exactBranchMatch.latestCommit.substring(0, 12);
|
||||
}
|
||||
|
||||
// Handles case when no branch is provided in the URL
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].id
|
||||
) {
|
||||
return commits.values[0].id.substring(0, 12);
|
||||
}
|
||||
|
||||
throw new Error(`Failed to read response from ${commitApiUrl}`);
|
||||
throw new Error(
|
||||
`Failed to find branch "${branch}" in property "displayId" of response to ${branchListUrl}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @backstage/backend-defaults
|
||||
|
||||
## 0.1.2-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/backend-plugin-api@0.1.3-next.1
|
||||
- @backstage/backend-app-api@0.2.2-next.1
|
||||
|
||||
## 0.1.2-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-defaults",
|
||||
"description": "Backend defaults used by Backstage backend apps",
|
||||
"version": "0.1.2-next.0",
|
||||
"version": "0.1.2-next.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# example-backend-next
|
||||
|
||||
## 0.0.4-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-scaffolder-backend@1.7.0-next.1
|
||||
- @backstage/backend-defaults@0.1.2-next.1
|
||||
- @backstage/plugin-app-backend@0.3.37-next.1
|
||||
- @backstage/plugin-catalog-backend@1.4.1-next.1
|
||||
|
||||
## 0.0.4-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend-next",
|
||||
"version": "0.0.4-next.0",
|
||||
"version": "0.0.4-next.1",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @backstage/backend-plugin-api
|
||||
|
||||
## 0.1.3-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a35a27df70: Added documentation for `createBackendModule`, with guidelines for choosing a module ID.
|
||||
- Updated dependencies
|
||||
- @backstage/backend-common@0.15.2-next.1
|
||||
- @backstage/backend-tasks@0.3.6-next.1
|
||||
- @backstage/config@1.0.3-next.1
|
||||
- @backstage/plugin-permission-common@0.6.5-next.1
|
||||
|
||||
## 0.1.3-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -71,7 +71,7 @@ export const cacheServiceRef: ServiceRef<PluginCacheManager, 'plugin'>;
|
||||
// @public (undocumented)
|
||||
export const configServiceRef: ServiceRef<Config, 'root'>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function createBackendModule<
|
||||
TOptions extends object | undefined = undefined,
|
||||
>(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-plugin-api",
|
||||
"description": "Core API used by Backstage backend plugins",
|
||||
"version": "0.1.3-next.0",
|
||||
"version": "0.1.3-next.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -69,7 +69,18 @@ export interface BackendModuleConfig<TOptions> {
|
||||
): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Creates a new backend module for a given plugin.
|
||||
*
|
||||
* The `moduleId` should be equal to the module-specific prefix of the exported name, such
|
||||
* that the full name is `moduleId + PluginId + "Module"`. For example, a GitHub entity
|
||||
* provider module for the `catalog` plugin might have the module ID `'githubEntityProvider'`,
|
||||
* and the full exported name would be `githubEntityProviderCatalogModule`.
|
||||
*
|
||||
* The `pluginId` should exactly match the `id` of the plugin that the module extends.
|
||||
*/
|
||||
export function createBackendModule<
|
||||
TOptions extends object | undefined = undefined,
|
||||
>(
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @backstage/backend-tasks
|
||||
|
||||
## 0.3.6-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/backend-common@0.15.2-next.1
|
||||
- @backstage/config@1.0.3-next.1
|
||||
- @backstage/errors@1.1.2-next.1
|
||||
- @backstage/types@1.0.0
|
||||
|
||||
## 0.3.6-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -30,6 +30,11 @@ export interface PluginTaskScheduler {
|
||||
triggerTask(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export function readTaskScheduleDefinitionFromConfig(
|
||||
config: Config,
|
||||
): TaskScheduleDefinition;
|
||||
|
||||
// @public
|
||||
export type TaskFunction =
|
||||
| ((abortSignal: AbortSignal_2) => void | Promise<void>)
|
||||
@@ -60,6 +65,19 @@ export interface TaskScheduleDefinition {
|
||||
timeout: Duration | HumanDuration;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface TaskScheduleDefinitionConfig {
|
||||
frequency:
|
||||
| {
|
||||
cron: string;
|
||||
}
|
||||
| string
|
||||
| HumanDuration;
|
||||
initialDelay?: string | HumanDuration;
|
||||
scope?: 'global' | 'local';
|
||||
timeout: string | HumanDuration;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class TaskScheduler {
|
||||
constructor(databaseManager: DatabaseManager, logger: Logger);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-tasks",
|
||||
"description": "Common distributed task management library for Backstage backends",
|
||||
"version": "0.3.6-next.0",
|
||||
"version": "0.3.6-next.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { readTaskScheduleDefinitionFromConfig } from './readTaskScheduleDefinitionFromConfig';
|
||||
export { TaskScheduler } from './TaskScheduler';
|
||||
export type {
|
||||
PluginTaskScheduler,
|
||||
@@ -21,5 +22,6 @@ export type {
|
||||
TaskInvocationDefinition,
|
||||
TaskRunner,
|
||||
TaskScheduleDefinition,
|
||||
TaskScheduleDefinitionConfig,
|
||||
HumanDuration,
|
||||
} from './types';
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 { ConfigReader } from '@backstage/config';
|
||||
import { Duration } from 'luxon';
|
||||
import { readTaskScheduleDefinitionFromConfig } from './readTaskScheduleDefinitionFromConfig';
|
||||
import { HumanDuration } from './types';
|
||||
|
||||
describe('readTaskScheduleDefinitionFromConfig', () => {
|
||||
it('all valid values', () => {
|
||||
const config = new ConfigReader({
|
||||
frequency: {
|
||||
cron: '0 30 * * * *',
|
||||
},
|
||||
timeout: 'PT3M',
|
||||
initialDelay: {
|
||||
minutes: 20,
|
||||
},
|
||||
scope: 'global',
|
||||
});
|
||||
|
||||
const result = readTaskScheduleDefinitionFromConfig(config);
|
||||
|
||||
expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *');
|
||||
expect(result.timeout).toEqual(Duration.fromISO('PT3M'));
|
||||
expect((result.initialDelay as HumanDuration).minutes).toEqual(20);
|
||||
expect(result.scope).toBe('global');
|
||||
});
|
||||
|
||||
it('all valid required values', () => {
|
||||
const config = new ConfigReader({
|
||||
frequency: {
|
||||
cron: '0 30 * * * *',
|
||||
},
|
||||
timeout: 'PT3M',
|
||||
});
|
||||
|
||||
const result = readTaskScheduleDefinitionFromConfig(config);
|
||||
|
||||
expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *');
|
||||
expect(result.timeout).toEqual(Duration.fromISO('PT3M'));
|
||||
expect(result.initialDelay).toBeUndefined();
|
||||
expect(result.scope).toBeUndefined();
|
||||
});
|
||||
|
||||
it('fail without required frequency', () => {
|
||||
const config = new ConfigReader({
|
||||
timeout: 'PT3M',
|
||||
});
|
||||
|
||||
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
|
||||
"Missing required config value at 'frequency'",
|
||||
);
|
||||
});
|
||||
|
||||
it('fail without required timeout', () => {
|
||||
const config = new ConfigReader({
|
||||
frequency: 'PT30M',
|
||||
});
|
||||
|
||||
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
|
||||
"Missing required config value at 'timeout'",
|
||||
);
|
||||
});
|
||||
|
||||
it('invalid frequency value', () => {
|
||||
const config = new ConfigReader({
|
||||
frequency: {
|
||||
invalid: 'value',
|
||||
},
|
||||
timeout: 'PT3M',
|
||||
});
|
||||
|
||||
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
|
||||
'HumanDuration needs at least one of',
|
||||
);
|
||||
});
|
||||
|
||||
it('frequency value with additional invalid prop', () => {
|
||||
const config = new ConfigReader({
|
||||
frequency: {
|
||||
minutes: 20,
|
||||
invalid: 'value',
|
||||
},
|
||||
timeout: 'PT3M',
|
||||
});
|
||||
|
||||
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
|
||||
'HumanDuration does not contain properties: invalid',
|
||||
);
|
||||
});
|
||||
|
||||
it('invalid scope value', () => {
|
||||
const config = new ConfigReader({
|
||||
frequency: {
|
||||
years: 2,
|
||||
},
|
||||
timeout: 'PT3M',
|
||||
scope: 'invalid',
|
||||
});
|
||||
|
||||
expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
|
||||
'Only "global" or "local" are allowed for TaskScheduleDefinition.scope, but got: invalid',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 { JsonObject } from '@backstage/types';
|
||||
import { HumanDuration, TaskScheduleDefinition } from './types';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
const propsOfHumanDuration = [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
'seconds',
|
||||
'milliseconds',
|
||||
];
|
||||
|
||||
function convertToHumanDuration(config: Config, key: string): HumanDuration {
|
||||
const props = config.getConfig(key).keys();
|
||||
if (!props.find(prop => propsOfHumanDuration.includes(prop))) {
|
||||
throw new Error(
|
||||
`HumanDuration needs at least one of: ${propsOfHumanDuration}`,
|
||||
);
|
||||
}
|
||||
|
||||
const invalidProps = props.filter(
|
||||
prop => !propsOfHumanDuration.includes(prop),
|
||||
);
|
||||
if (invalidProps.length > 0) {
|
||||
throw new Error(
|
||||
`HumanDuration does not contain properties: ${invalidProps}`,
|
||||
);
|
||||
}
|
||||
|
||||
return config.get<JsonObject>(key) as HumanDuration;
|
||||
}
|
||||
|
||||
function readDuration(config: Config, key: string): Duration | HumanDuration {
|
||||
return typeof config.get(key) === 'string'
|
||||
? Duration.fromISO(config.getString(key))
|
||||
: convertToHumanDuration(config, key);
|
||||
}
|
||||
|
||||
function readCronOrDuration(
|
||||
config: Config,
|
||||
key: string,
|
||||
): { cron: string } | Duration | HumanDuration {
|
||||
const value = config.get(key);
|
||||
if (typeof value === 'object' && (value as { cron?: string }).cron) {
|
||||
return value as { cron: string };
|
||||
}
|
||||
|
||||
return readDuration(config, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a TaskScheduleDefinition from a Config.
|
||||
* Expects the config not to be the root config,
|
||||
* but the config for the definition.
|
||||
*
|
||||
* @param config - config for a TaskScheduleDefinition.
|
||||
* @public
|
||||
*/
|
||||
export function readTaskScheduleDefinitionFromConfig(
|
||||
config: Config,
|
||||
): TaskScheduleDefinition {
|
||||
const frequency = readCronOrDuration(config, 'frequency');
|
||||
const timeout = readDuration(config, 'timeout');
|
||||
|
||||
const initialDelay = config.has('initialDelay')
|
||||
? readDuration(config, 'initialDelay')
|
||||
: undefined;
|
||||
|
||||
const scope = config.getOptionalString('scope');
|
||||
if (scope && !['global', 'local'].includes(scope)) {
|
||||
throw new Error(
|
||||
`Only "global" or "local" are allowed for TaskScheduleDefinition.scope, but got: ${scope}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
frequency,
|
||||
timeout,
|
||||
initialDelay,
|
||||
scope: scope as 'global' | 'local' | undefined,
|
||||
};
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export interface TaskScheduleDefinition {
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This is a best effort value; under some circumstances there can be
|
||||
* This is the best effort value; under some circumstances there can be
|
||||
* deviations. For example, if the task runtime is longer than the frequency
|
||||
* and the timeout has not been given or not been exceeded yet, the next
|
||||
* invocation of this task will be delayed until after the previous one
|
||||
@@ -112,7 +112,7 @@ export interface TaskScheduleDefinition {
|
||||
* collaborating on a task that has its `scope` field set to `'global'`, then
|
||||
* you may still see the task being processed by other long-lived workers,
|
||||
* while any given single worker is in its initial sleep delay time e.g. after
|
||||
* a deployment. Therefore this parameter is not useful for "globally" pausing
|
||||
* a deployment. Therefore, this parameter is not useful for "globally" pausing
|
||||
* work; its main intended use is for individual machines to get a chance to
|
||||
* reach some equilibrium at startup before triggering heavy batch workloads.
|
||||
*/
|
||||
@@ -128,7 +128,104 @@ export interface TaskScheduleDefinition {
|
||||
* attempt to ensure that only one worker machine runs the task at a time,
|
||||
* according to the given cadence. This means that as the number of worker
|
||||
* hosts increases, the invocation frequency of this task will not go up.
|
||||
* Instead the load is spread randomly across hosts. This setting is useful
|
||||
* Instead, the load is spread randomly across hosts. This setting is useful
|
||||
* for tasks that access shared resources, for example catalog ingestion tasks
|
||||
* where you do not want many machines to repeatedly import the same data and
|
||||
* trample over each other.
|
||||
*
|
||||
* When the scope is set to `'local'`, there is no concurrency control across
|
||||
* hosts. Each host runs the task according to the given cadence similarly to
|
||||
* `setInterval`, but the runtime ensures that there are no overlapping runs.
|
||||
*
|
||||
* @defaultValue 'global'
|
||||
*/
|
||||
scope?: 'global' | 'local';
|
||||
}
|
||||
|
||||
/**
|
||||
* Config options for {@link TaskScheduleDefinition}
|
||||
* that control the scheduling of a task.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TaskScheduleDefinitionConfig {
|
||||
/**
|
||||
* How often you want the task to run. The system does its best to avoid
|
||||
* overlapping invocations.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This is the best effort value; under some circumstances there can be
|
||||
* deviations. For example, if the task runtime is longer than the frequency
|
||||
* and the timeout has not been given or not been exceeded yet, the next
|
||||
* invocation of this task will be delayed until after the previous one
|
||||
* finishes.
|
||||
*
|
||||
* This is a required field.
|
||||
*/
|
||||
frequency:
|
||||
| {
|
||||
/**
|
||||
* A crontab style string.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Overview:
|
||||
*
|
||||
* ```
|
||||
* ┌────────────── second (optional)
|
||||
* │ ┌──────────── minute
|
||||
* │ │ ┌────────── hour
|
||||
* │ │ │ ┌──────── day of month
|
||||
* │ │ │ │ ┌────── month
|
||||
* │ │ │ │ │ ┌──── day of week
|
||||
* │ │ │ │ │ │
|
||||
* │ │ │ │ │ │
|
||||
* * * * * * *
|
||||
* ```
|
||||
*/
|
||||
cron: string;
|
||||
}
|
||||
| string
|
||||
| HumanDuration;
|
||||
|
||||
/**
|
||||
* The maximum amount of time that a single task invocation can take, before
|
||||
* it's considered timed out and gets "released" such that a new invocation
|
||||
* is permitted to take place (possibly, then, on a different worker).
|
||||
*/
|
||||
timeout: string | HumanDuration;
|
||||
|
||||
/**
|
||||
* The amount of time that should pass before the first invocation happens.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This can be useful in cold start scenarios to stagger or delay some heavy
|
||||
* compute jobs. If no value is given for this field then the first invocation
|
||||
* will happen as soon as possible according to the cadence.
|
||||
*
|
||||
* NOTE: This is a per-worker delay. If you have a cluster of workers all
|
||||
* collaborating on a task that has its `scope` field set to `'global'`, then
|
||||
* you may still see the task being processed by other long-lived workers,
|
||||
* while any given single worker is in its initial sleep delay time e.g. after
|
||||
* a deployment. Therefore, this parameter is not useful for "globally" pausing
|
||||
* work; its main intended use is for individual machines to get a chance to
|
||||
* reach some equilibrium at startup before triggering heavy batch workloads.
|
||||
*/
|
||||
initialDelay?: string | HumanDuration;
|
||||
|
||||
/**
|
||||
* Sets the scope of concurrency control / locking to apply for invocations of
|
||||
* this task.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* When the scope is set to the default value `'global'`, the scheduler will
|
||||
* attempt to ensure that only one worker machine runs the task at a time,
|
||||
* according to the given cadence. This means that as the number of worker
|
||||
* hosts increases, the invocation frequency of this task will not go up.
|
||||
* Instead, the load is spread randomly across hosts. This setting is useful
|
||||
* for tasks that access shared resources, for example catalog ingestion tasks
|
||||
* where you do not want many machines to repeatedly import the same data and
|
||||
* trample over each other.
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @backstage/backend-test-utils
|
||||
|
||||
## 0.1.29-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/cli@0.20.0-next.1
|
||||
- @backstage/backend-common@0.15.2-next.1
|
||||
- @backstage/backend-plugin-api@0.1.3-next.1
|
||||
- @backstage/backend-app-api@0.2.2-next.1
|
||||
- @backstage/config@1.0.3-next.1
|
||||
|
||||
## 0.1.29-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-test-utils",
|
||||
"description": "Test helpers library for Backstage backends",
|
||||
"version": "0.1.29-next.0",
|
||||
"version": "0.1.29-next.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
# example-backend
|
||||
|
||||
## 0.2.76-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-auth-backend@0.17.0-next.1
|
||||
- @backstage/plugin-search-backend@1.1.0-next.1
|
||||
- @backstage/catalog-client@1.1.1-next.1
|
||||
- @backstage/backend-common@0.15.2-next.1
|
||||
- @backstage/plugin-scaffolder-backend@1.7.0-next.1
|
||||
- @backstage/plugin-search-common@1.1.0-next.1
|
||||
- @backstage/plugin-search-backend-node@1.0.3-next.1
|
||||
- @backstage/plugin-search-backend-module-pg@0.4.1-next.1
|
||||
- @backstage/plugin-search-backend-module-elasticsearch@1.0.3-next.1
|
||||
- @backstage/plugin-kubernetes-backend@0.7.3-next.1
|
||||
- @backstage/plugin-tech-insights-backend@0.5.3-next.1
|
||||
- example-app@0.2.76-next.1
|
||||
- @backstage/backend-tasks@0.3.6-next.1
|
||||
- @backstage/catalog-model@1.1.2-next.1
|
||||
- @backstage/config@1.0.3-next.1
|
||||
- @backstage/integration@1.3.2-next.1
|
||||
- @backstage/plugin-app-backend@0.3.37-next.1
|
||||
- @backstage/plugin-auth-node@0.2.6-next.1
|
||||
- @backstage/plugin-azure-devops-backend@0.3.16-next.1
|
||||
- @backstage/plugin-badges-backend@0.1.31-next.1
|
||||
- @backstage/plugin-catalog-backend@1.4.1-next.1
|
||||
- @backstage/plugin-code-coverage-backend@0.2.3-next.1
|
||||
- @backstage/plugin-graphql-backend@0.1.27-next.1
|
||||
- @backstage/plugin-jenkins-backend@0.1.27-next.1
|
||||
- @backstage/plugin-kafka-backend@0.2.30-next.1
|
||||
- @backstage/plugin-permission-backend@0.5.12-next.1
|
||||
- @backstage/plugin-permission-common@0.6.5-next.1
|
||||
- @backstage/plugin-permission-node@0.6.6-next.1
|
||||
- @backstage/plugin-playlist-backend@0.1.1-next.1
|
||||
- @backstage/plugin-proxy-backend@0.2.31-next.1
|
||||
- @backstage/plugin-rollbar-backend@0.1.34-next.1
|
||||
- @backstage/plugin-scaffolder-backend-module-rails@0.4.5-next.1
|
||||
- @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.21-next.1
|
||||
- @backstage/plugin-tech-insights-node@0.3.5-next.1
|
||||
- @backstage/plugin-techdocs-backend@1.3.1-next.1
|
||||
- @backstage/plugin-todo-backend@0.1.34-next.1
|
||||
|
||||
## 0.2.76-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend",
|
||||
"version": "0.2.76-next.0",
|
||||
"version": "0.2.76-next.1",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user