Merge branch 'backstage:master' into master

This commit is contained in:
djamaile
2021-12-20 13:28:48 +01:00
committed by GitHub
304 changed files with 5483 additions and 1746 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-github-actions': patch
---
Show empty state only when workflow API call has completed
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/app-defaults': patch
'@backstage/core-app-api': patch
'@backstage/core-plugin-api': patch
---
Add `FetchApi` and related `fetchApiRef` which implement fetch, with an added Backstage token header when available.
+22
View File
@@ -0,0 +1,22 @@
---
'@backstage/backend-common': patch
---
Fixed bug in backend-common to allow passing of remote option in order to enable passing remote url in --config option. The remote option should be passed along with reloadIntervalSeconds from packages/backend/src/index.ts (Updated the file as well)
These changes are needed in `packages/backend/src/index.ts` if remote URLs are desired to be passed in --config option and read and watch remote files for config.
```diff
@@ -86,7 +86,11 @@ async function main() {
const config = await loadBackendConfig({
argv: process.argv,
logger,
+ remote: {
+ reloadIntervalSeconds: 60 * 10 // Check remote config changes every 10 minutes. Change to your desired interval in seconds
+ }
});
+
const createEnv = makeCreateEnv(config);
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/config-loader': patch
---
In case remote.reloadIntervalSeconds is passed, it must be a valid positive value
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-user-settings': patch
---
Fix undefined identity bug in UserSettingsProfileCard caused by using deprecated methods of the IdentityApi
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
added useOwnedEntities hook to get the list of entities of the logged-in user
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Fixed bug in OwnershipCard component where text wasn't correctly pluralized
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-rails': patch
---
Add new options to rails new (force and skipTests)
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-common': minor
---
Create catalog-common and add catalog permissions.
+25
View File
@@ -0,0 +1,25 @@
---
'@backstage/plugin-catalog': patch
---
Deprecated the `CatalogClientWrapper` class.
The default implementation of `catalogApiRef` that this plugin exposes, is now powered by the new `fetchApiRef`. The default implementation of _that_ API, in turn, has the ability to inject the user's Backstage token in requests in a similar manner to what the deprecated `CatalogClientWrapper` used to do. The latter has therefore been taken out of the default catalog API implementation.
If you use a custom `fetchApiRef` implementation that does NOT issue tokens, or use a custom `catalogApiRef` implementation which does NOT use the default `fetchApiRef`, you can still for some time wrap your catalog API in this class to get back the old behavior:
```ts
// Add this to your packages/app/src/plugins.ts if you want to get back the old
// catalog client behavior:
createApiFactory({
api: catalogApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) =>
new CatalogClientWrapper({
client: new CatalogClient({ discoveryApi }),
identityApi,
}),
}),
```
But do consider migrating to making use of the `fetchApiRef` as soon as convenient, since the wrapper class will be removed in a future release.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': patch
---
Add the ability to supply a custom `fetchApi`. In the default frontend app setup, this will use the `fetchApiRef` implementation.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/test-utils': minor
---
Removed deprecated `Keyboard` class which has been superseded by `@testing-library/user-event#userEvent`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/config-loader': minor
---
Removed deprecated option `configPaths` as it has been superseded by `configTargets`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Added OwnedEntityPicker field which displays Owned Entities in options
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Fixed the fallback identity population to correctly generate an entity reference for `userEntityRef` if no token is provided.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': patch
---
Add support for passing paging parameters to the getEntities call of the catalog client
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-search-backend': minor
---
Search result location filtering
This change introduces a filter for search results based on their location protocol. The intention is to filter out unsafe or
malicious values before they can be consumed by the frontend. By default locations must be http/https URLs (or paths).
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/core-components': patch
'@backstage/plugin-scaffolder': patch
---
In @backstage/plugin-scaffolder - When user will have one option available in hostUrl or owner - autoselect and select component should be readonly.
in @backstage/core-components - Select component has extended API with few more props: native : boolean, disabled: boolean. native - if set to true - Select component will use native browser select picker (not rendered by Material UI lib ).
disabled - if set to true - action on component will not be possible.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-jenkins-backend': patch
---
feature: add crumbIssuer option to Jenkins (optional) configuration, improve the UI to show a notification after executing the action re-build
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-jenkins': patch
---
feature: add crumbIssuer option to Jenkins (optional) configuration, improve the UI to show a notification after executing the action re-build
-14
View File
@@ -1,14 +0,0 @@
---
'@backstage/plugin-apache-airflow': minor
---
Introduces a new plugin for the Apache Airflow workflow management platform.
This implementation has been tested with the Apache Airflow v2 API,
authenticating with basic authentication through the Backstage proxy plugin.
Supported functionality includes:
- Information card of version information of the Airflow instance
- Information card of instance health for the meta-database and scheduler
- Table of DAGs with meta information and status, along with a link to view
details in the Airflow UI
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': minor
---
Removed deprecated types `ApiRefType` and `ApiRefsToTypes`
+11
View File
@@ -0,0 +1,11 @@
catalog:
- plugins/catalog/**/*
- plugins/catalog-*/**/*
- packages/catalog-*/**/*
scaffolder:
- plugins/scaffolder/**/*
- plugins/scaffolder-*/**/*
search:
- plugins/search/**/*
- plugins/search-*/**/*
- packages/search-*/**/*
+2
View File
@@ -12,6 +12,7 @@ Atlassian
automations
autoscaling
Autoscaling
autoselect
Avro
backrub
Bigtable
@@ -53,6 +54,7 @@ configs
const
cookiecutter
cron
cronjobs
css
Datadog
dataflow
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
name: Create Changeset PR
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Install Dependencies
run: yarn --frozen-lockfile
- name: Create Release Pull Request
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
uses: actions/checkout@v2
- name: Install Fossa
run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash"
+12
View File
@@ -0,0 +1,12 @@
name: 'Pull Request Labeler'
on:
- pull_request_target
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v3
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
sync-labels: true
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
recent activity from the author. It will be closed if no further activity occurs.
If you are the author and the PR has been closed, feel free to re-open the PR and continue the contribution!
days-before-pr-stale: 7
days-before-pr-close: 3
days-before-pr-close: 5
exempt-pr-labels: reviewer-approved,awaiting-review
stale-pr-label: stale
operations-per-run: 100
+4
View File
@@ -2,6 +2,10 @@
# [Backstage](https://backstage.io)
> 🎄 The maintainers will be taking a break over the holidays from week beginning 20th Dec. The Repository and Discord may be quieter than usual. We will be back next year, rested and restored, on Jan. 3. 🎄
> 🎅 Happy holidays and a Happy New Year to all, and especially those that have made this year special for Backstage! 🎅
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![CNCF Status](https://img.shields.io/badge/cncf%20status-sandbox-blue.svg)](https://www.cncf.io/projects)
[![Main CI Build](https://github.com/backstage/backstage/workflows/Main%20Master%20Build/badge.svg)](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22)
+122
View File
@@ -61,3 +61,125 @@ breaking change to make `theme` optional. This means that if you currently
construct the themes that you pass on to `createApp` using `AppTheme` as an
intermediate type, you will need to work around this in some way, for example by
passing the themes to `createApp` more directly.
### Generic Auth API Refs
`Released 2021-12-16 in @backstage/core-plugin-api v0.3.1`
There are four auth Utility API references in `@backstage/core-plugin-api` that
were too generic to be useful. The APIs in question are `auth0AuthApiRef`,
`oauth2ApiRef`, `oidcAuthApiRef`, and `samlAuthApiRef`. The issue with these
APIs was that they had no actual contract of what the backing auth provider was.
This made it more or less impossible to use these providers in open source
plugins in any meaningful way. We also did not want to keep these Utility API
references around just as helpers either, instead opting to remove them and let
integrators define their own APIs that are more specific to their auth provider.
This is also falls in line with a long-term goal to unify all auth providers to
not have separate frontend implementations.
If you're currently using one of these API references for either Sign-In or
access delegation within an app, there are a couple of steps you need to take to
migrate to your own custom API.
First, you'll need to define a new Utility API reference. If you're only using
the API for sign-in, you can put the definition in `packages/app/src/apis.ts`.
However, if you need to access your auth API inside plugins you you'll need to
export it from a common package. If you don't already have one we recommended
creating `@internal/apis` and from there export the API reference.
```ts
// `ProfileInfoApi & BackstageIdentityApi & SessionApi` are required for sign-in
// Include `OAuthApi & OpenIdConnectApi` only if applicable
export const acmeAuthApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
> = createApiRef({
id: 'internal.auth.acme',
});
```
Next you'll want to wire up the API inside `packages/app/src/apis.ts`, which
varies depending on which API you're replacing. If you for example are replacing
the `oauth2ApiRef`, the factory might look like this:
```ts
// oauth2
createApiFactory({
api: acmeAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OAuth2.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
});
```
Provider specific factory implementations, copy the code you need into the
factory method depending on which apiRef you previously used.
```ts
// samlAuthApiRef
SamlAuth.create({
discoveryApi,
environment: configApi.getOptionalString('auth.environment'),
});
// oidcAuthApiRef
OAuth2.create({
discoveryApi,
oauthRequestApi,
provider: {
id: 'oidc',
title: 'Your Identity Provider',
icon: () => null,
},
environment: configApi.getOptionalString('auth.environment'),
});
// auth0AuthApiRef
OAuth2.create({
discoveryApi,
oauthRequestApi,
provider: {
id: 'auth0',
title: 'Auth0',
icon: () => null,
},
defaultScopes: ['openid', 'email', 'profile'],
environment: configApi.getOptionalString('auth.environment'),
});
```
Finally, for the provider to show up in your settings menu, you also need to
update the settings route in `packages/app/src/App.tsx` to pass the
`acmeAuthApiRef` to the `UserSettingsPage`. This replaces all existing provider
items, so you might want to add back any of the default ones that you are using
from the
[DefaultProviderSettings](https://github.com/backstage/backstage/blob/a3ec122170e0205fd3f9c307b98b1c5e4f55bf5f/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx#L35).
```tsx
<Route
path="/settings"
element={
<UserSettingsPage
providerSettings={
<ProviderSettingsItem
title="ACME"
description="Provides sign-in via ACME"
apiRef={acmeAuthApiRef}
icon={Star}
/>
}
/>
}
/>
```
+1 -1
View File
@@ -48,6 +48,6 @@ The Microsoft provider is a structure with three configuration keys:
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `microsoftAuthApi` reference and
To add the provider to the frontend, add the `microsoftAuthApiRef` reference and
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
+4 -1
View File
@@ -67,7 +67,7 @@ production build.
## Configuration Files
It is possible to have multiple configuration files (bundled and/or remote),
It is possible to have multiple configuration files (bundled and/or remote\*),
both to support different environments, but also to define configuration that is
local to specific packages. The configuration files to load are selected using a
`--config <local-path|url>` flag, and it is possible to load any number of
@@ -77,6 +77,9 @@ root when running the backend, you would use `--config ../../my-config.yaml`,
and for config file on a config server you would use
`--config https://some.domain.io/app-config.yaml`
**Note**: In case URLs are passed, it is also needed to set the remote option in
the loadBackendConfig call.
If no `config` flags are specified, the default behavior is to load
`app-config.yaml` and, if it exists, `app-config.local.yaml` from the repo root.
In the provided project setup, `app-config.local.yaml` is `.gitignore`'d, making
@@ -264,6 +264,12 @@ following objects:
- horizontalpodautoscalers
- ingresses
The following RBAC permissions are required on the batch API group for the
following objects:
- jobs
- cronjobs
## Surfacing your Kubernetes components as part of an entity
There are two ways to surface your Kubernetes components as part of an entity.
+12
View File
@@ -90,6 +90,18 @@ within your instance. The configuration options are documented in the
The underlying functionality is using official ElasticSearch client version 7.x,
meaning that ElasticSearch version 7 is the only one confirmed to be supported.
Should you need to create your own bespoke search experiences that require more
than just a query translator (such as faceted search or Relay pagination), you
can access the configuration of the search engine in order to create new elastic
search clients. The version of the client need not be the same as one used
internally by the elastic search engine plugin. For example:
```typescript
import { Client } from '@elastic/elastic-search';
const client = searchEngine.newClient(options => new Client(options));
```
## Example configurations
### AWS
@@ -26,7 +26,7 @@ You can create your own Field Extension by using the
`API` like below:
```tsx
//packages/app/scaffolder/MyCustomExtension/MyCustomExtension.tsx
//packages/app/src/scaffolder/MyCustomExtension/MyCustomExtension.tsx
import React from 'react';
import { FieldProps, FieldValidation } from '@rjsf/core';
import FormControl from '@material-ui/core/FormControl';
@@ -68,7 +68,7 @@ export const myCustomValidation = (
```
```tsx
// packages/app/scaffolder/MyCustomExtension/extensions.ts
// packages/app/src/scaffolder/MyCustomExtension/extensions.ts
/*
This is where the magic happens and creates the custom field extension.
@@ -94,7 +94,7 @@ export const MyCustomFieldExtension = plugin.provide(
```
```tsx
// packages/app/scaffolder/MyCustomExtension/index.ts
// packages/app/src/scaffolder/MyCustomExtension/index.ts
export { MyCustomFieldExtension } from './extension';
```
@@ -102,7 +102,7 @@ export { MyCustomFieldExtension } from './extension';
Once all these files are in place, you then need to provide your custom
extension to the `scaffolder` plugin.
You do this in `packages/app/App.tsx`. You need to provide the
You do this in `packages/app/src/App.tsx`. You need to provide the
`customFieldExtensions` as children to the `ScaffolderPage`.
```tsx
@@ -118,7 +118,7 @@ const routes = (
Should look something like this instead:
```tsx
import { MyCustomFieldExtension } from './scafffolder/MyCustomExtension';
import { MyCustomFieldExtension } from './scaffolder/MyCustomExtension';
const routes = (
<FlatRoutes>
...
+6 -1
View File
@@ -1,4 +1,9 @@
# TechDocs CLI
---
id: cli
title: TechDocs CLI
# prettier-ignore
description: TechDocs CLI - a utility command line interface for managing TechDocs sites in Backstage.
---
Utility command line interface for managing TechDocs sites in
[Backstage](https://github.com/backstage/backstage).
+5 -4
View File
@@ -55,9 +55,9 @@
},
"version": "1.0.0",
"dependencies": {
"@microsoft/api-documenter": "^7.13.68",
"@microsoft/api-extractor": "^7.18.7",
"@microsoft/api-extractor-model": "^7.13.5",
"@microsoft/api-documenter": "^7.13.77",
"@microsoft/api-extractor": "^7.19.2",
"@microsoft/api-extractor-model": "^7.15.1",
"@microsoft/tsdoc": "^0.13.2"
},
"devDependencies": {
@@ -78,11 +78,12 @@
"prettier": "^2.2.1",
"shx": "^0.3.2",
"ts-node": "^10.4.0",
"typescript": "~4.3.5",
"yarn-lock-check": "^1.0.5"
},
"prettier": "@spotify/prettier-config",
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"*.{js,jsx,ts,tsx,mjs,cjs}": [
"eslint --fix",
"prettier --write"
],
@@ -34,6 +34,8 @@ import {
OneLoginAuth,
UnhandledErrorForwarder,
AtlassianAuth,
createFetchApi,
FetchMiddlewares,
} from '@backstage/core-app-api';
import {
@@ -42,6 +44,8 @@ import {
analyticsApiRef,
errorApiRef,
discoveryApiRef,
fetchApiRef,
identityApiRef,
oauthRequestApiRef,
googleAuthApiRef,
githubAuthApiRef,
@@ -92,6 +96,27 @@ export const apis = [
deps: { errorApi: errorApiRef },
factory: ({ errorApi }) => WebStorage.create({ errorApi }),
}),
createApiFactory({
api: fetchApiRef,
deps: {
configApi: configApiRef,
identityApi: identityApiRef,
discoveryApi: discoveryApiRef,
},
factory: ({ configApi, identityApi, discoveryApi }) => {
return createFetchApi({
middleware: [
FetchMiddlewares.resolvePluginProtocol({
discoveryApi,
}),
FetchMiddlewares.injectIdentityAuth({
identityApi,
config: configApi,
}),
],
});
},
}),
createApiFactory({
api: oauthRequestApiRef,
deps: {},
+18
View File
@@ -1,5 +1,23 @@
# example-app
## 0.2.57
### Patch Changes
- Updated dependencies
- @backstage/plugin-github-actions@0.4.27
- @backstage/core-app-api@0.2.1
- @backstage/plugin-kubernetes@0.5.1
- @backstage/core-plugin-api@0.3.1
- @backstage/core-components@0.8.1
- @backstage/plugin-org@0.3.31
- @backstage/plugin-azure-devops@0.1.7
- @backstage/cli@0.10.2
- @backstage/catalog-model@0.9.8
- @backstage/plugin-techdocs@0.12.10
- @backstage/plugin-catalog-react@0.6.7
- @backstage/plugin-apache-airflow@0.1.0
## 0.2.56
### Patch Changes
+13 -13
View File
@@ -1,39 +1,39 @@
{
"name": "example-app",
"version": "0.2.56",
"version": "0.2.57",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/app-defaults": "^0.1.2",
"@backstage/catalog-model": "^0.9.7",
"@backstage/cli": "^0.10.1",
"@backstage/core-app-api": "^0.2.0",
"@backstage/core-components": "^0.8.0",
"@backstage/core-plugin-api": "^0.3.0",
"@backstage/catalog-model": "^0.9.8",
"@backstage/cli": "^0.10.2",
"@backstage/core-app-api": "^0.2.1",
"@backstage/core-components": "^0.8.1",
"@backstage/core-plugin-api": "^0.3.1",
"@backstage/integration-react": "^0.1.15",
"@backstage/plugin-api-docs": "^0.6.18",
"@backstage/plugin-azure-devops": "^0.1.6",
"@backstage/plugin-apache-airflow": "^0.0.0",
"@backstage/plugin-azure-devops": "^0.1.7",
"@backstage/plugin-apache-airflow": "^0.1.0",
"@backstage/plugin-badges": "^0.2.16",
"@backstage/plugin-catalog": "^0.7.4",
"@backstage/plugin-catalog-graph": "^0.2.3",
"@backstage/plugin-catalog-import": "^0.7.5",
"@backstage/plugin-catalog-react": "^0.6.5",
"@backstage/plugin-catalog-react": "^0.6.7",
"@backstage/plugin-circleci": "^0.2.31",
"@backstage/plugin-cloudbuild": "^0.2.29",
"@backstage/plugin-code-coverage": "^0.1.19",
"@backstage/plugin-cost-insights": "^0.11.13",
"@backstage/plugin-explore": "^0.3.22",
"@backstage/plugin-gcp-projects": "^0.3.10",
"@backstage/plugin-github-actions": "^0.4.26",
"@backstage/plugin-github-actions": "^0.4.27",
"@backstage/plugin-graphiql": "^0.2.24",
"@backstage/plugin-home": "^0.4.7",
"@backstage/plugin-jenkins": "^0.5.14",
"@backstage/plugin-kafka": "^0.2.22",
"@backstage/plugin-kubernetes": "^0.5.0",
"@backstage/plugin-kubernetes": "^0.5.1",
"@backstage/plugin-lighthouse": "^0.2.31",
"@backstage/plugin-newrelic": "^0.3.10",
"@backstage/plugin-org": "^0.3.30",
"@backstage/plugin-org": "^0.3.31",
"@backstage/plugin-pagerduty": "0.3.19",
"@backstage/plugin-rollbar": "^0.3.20",
"@backstage/plugin-scaffolder": "^0.11.14",
@@ -41,7 +41,7 @@
"@backstage/plugin-sentry": "^0.3.30",
"@backstage/plugin-shortcuts": "^0.1.15",
"@backstage/plugin-tech-radar": "^0.4.13",
"@backstage/plugin-techdocs": "^0.12.9",
"@backstage/plugin-techdocs": "^0.12.10",
"@backstage/plugin-todo": "^0.1.16",
"@backstage/plugin-user-settings": "^0.3.13",
"@backstage/search-common": "^0.2.0",
@@ -126,6 +126,8 @@ import {
} from '@roadiehq/backstage-plugin-travis-ci';
import React, { ReactNode, useMemo, useState } from 'react';
const customEntityFilterKind = ['Component', 'API', 'System'];
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
const [badgesDialogOpen, setBadgesDialogOpen] = useState(false);
@@ -523,7 +525,10 @@ const userPage = (
<EntityUserProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
<EntityOwnershipCard
variant="gridItem"
entityFilterKind={customEntityFilterKind}
/>
</Grid>
</Grid>
</EntityLayout.Route>
@@ -539,7 +544,10 @@ const groupPage = (
<EntityGroupProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
<EntityOwnershipCard
variant="gridItem"
entityFilterKind={customEntityFilterKind}
/>
</Grid>
<Grid item xs={12}>
<EntityMembersListCard />
+7
View File
@@ -1,5 +1,12 @@
# @backstage/backend-common
## 0.9.14
### Patch Changes
- fe24bc9a32: Each plugin now saves to a separate sqlite database file when `connection.filename` is provided in the sqlite config.
Any existing sqlite database files will be ignored.
## 0.9.13
### Patch Changes
+2
View File
@@ -21,6 +21,7 @@ import { GitLabIntegration } from '@backstage/integration';
import { isChildPath } from '@backstage/cli-common';
import { JsonValue } from '@backstage/types';
import { Knex } from 'knex';
import { LoadConfigOptionsRemote } from '@backstage/config-loader';
import { Logger as Logger_2 } from 'winston';
import { MergeResult } from 'isomorphic-git';
import { PushResult } from 'isomorphic-git';
@@ -338,6 +339,7 @@ export function isDatabaseConflictError(e: unknown): boolean;
// @public
export function loadBackendConfig(options: {
logger: Logger_2;
remote?: LoadConfigOptionsRemote;
argv: string[];
}): Promise<Config>;
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.9.13",
"version": "0.9.14",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -81,7 +81,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.10.1",
"@backstage/cli": "^0.10.2",
"@backstage/test-utils": "^0.1.24",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
@@ -94,7 +94,7 @@
"@types/recursive-readdir": "^2.2.0",
"@types/stoppable": "^1.1.0",
"@types/supertest": "^2.0.8",
"@types/tar": "^4.0.3",
"@types/tar": "^6.1.1",
"@types/unzipper": "^0.10.3",
"@types/webpack-env": "^1.15.2",
"aws-sdk-mock": "^5.2.1",
+3 -1
View File
@@ -23,6 +23,7 @@ import {
loadConfig,
ConfigSchema,
ConfigTarget,
LoadConfigOptionsRemote,
} from '@backstage/config-loader';
import { AppConfig, Config, ConfigReader } from '@backstage/config';
import { JsonValue } from '@backstage/types';
@@ -178,6 +179,7 @@ let currentCancelFunc: () => void;
export async function loadBackendConfig(options: {
logger: Logger;
// process.argv or any other overrides
remote?: LoadConfigOptionsRemote;
argv: string[];
}): Promise<Config> {
const args = parseArgs(options.argv);
@@ -202,8 +204,8 @@ export async function loadBackendConfig(options: {
const config = new ObservableConfigProxy(options.logger);
const { appConfigs } = await loadConfig({
configRoot: paths.targetRoot,
configPaths: [],
configTargets: configTargets,
remote: options.remote,
watch: {
onChange(newConfigs) {
options.logger.info(
@@ -15,6 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { omit } from 'lodash';
import path from 'path';
import {
createDatabaseClient,
ensureDatabaseExists,
@@ -170,28 +171,6 @@ describe('DatabaseManager', () => {
);
});
it('uses top level sqlite database filename if plugin config is not present', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'sqlite3',
connection: 'some-file-path',
},
},
}),
);
await testManager.forPlugin('pluginwithoutconfig').getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [_, overrides] = mockCalls[0];
expect(overrides).toHaveProperty(
'connection.filename',
expect.stringContaining('some-file-path'),
);
});
it('provides an inmemory sqlite database if top level is also inmemory and plugin config is not present', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
@@ -214,6 +193,110 @@ describe('DatabaseManager', () => {
);
});
it('throws if top level sqlite filename is provided', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'sqlite3',
connection: 'some-file-path',
},
},
}),
);
await expect(
testManager.forPlugin('pluginwithoutconfig').getClient(),
).rejects.toBeInstanceOf(Error);
});
it('creates plugin-specific sqlite files when plugin config is not present', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'sqlite3',
connection: {
directory: 'sqlite-files',
},
},
},
}),
);
await testManager.forPlugin('pluginwithoutconfig').getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [_, overrides] = mockCalls[0];
expect(overrides).toHaveProperty(
'connection.filename',
path.join('sqlite-files', 'pluginwithoutconfig.sqlite'),
);
});
it('uses sqlite directory from top level config and filename from plugin config', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'sqlite3',
connection: {
directory: 'sqlite-files',
},
plugin: {
test: {
connection: {
filename: 'other.sqlite',
},
},
},
},
},
}),
);
await testManager.forPlugin('test').getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [_, overrides] = mockCalls[0];
expect(overrides).toHaveProperty(
'connection.filename',
path.join('sqlite-files', 'other.sqlite'),
);
});
it('uses sqlite directory and filename from plugin config', async () => {
const testManager = DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: {
client: 'sqlite3',
connection: {
directory: 'sqlite-files',
},
plugin: {
test: {
connection: {
directory: 'custom-sqlite-files',
filename: 'other.sqlite',
},
},
},
},
},
}),
);
await testManager.forPlugin('test').getClient();
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
const [_, overrides] = mockCalls[0];
expect(overrides).toHaveProperty(
'connection.filename',
path.join('custom-sqlite-files', 'other.sqlite'),
);
});
it('connects to a plugin database using a specific database name', async () => {
// testdbname.connection.database is set in config
await manager.forPlugin('testdbname').getClient();
@@ -282,7 +365,10 @@ describe('DatabaseManager', () => {
expect(baseConfig.get().client).toEqual('sqlite3');
// sqlite3 uses 'filename' instead of 'database'
expect(overrides).toHaveProperty('connection.filename');
expect(overrides).toHaveProperty(
'connection.filename',
'plugin_with_different_client',
);
});
it('provides database client specific base from plugin connection string when client set under plugin', async () => {
@@ -28,6 +28,7 @@ import {
normalizeConnection,
} from './connection';
import { PluginDatabaseManager } from './types';
import path from 'path';
/**
* Provides a config lookup path for a plugin's config block.
@@ -114,10 +115,18 @@ export class DatabaseManager {
const connection = this.getConnectionConfig(pluginId);
if (this.getClientType(pluginId).client === 'sqlite3') {
// sqlite database name should fallback to ':memory:' as a special case
return (
(connection as Knex.Sqlite3ConnectionConfig)?.filename ?? ':memory:'
);
const sqliteFilename: string | undefined = (
connection as Knex.Sqlite3ConnectionConfig
).filename;
if (sqliteFilename === ':memory:') {
return sqliteFilename;
}
const sqliteDirectory =
(connection as { directory?: string }).directory ?? '.';
return path.join(sqliteDirectory, sqliteFilename ?? `${pluginId}.sqlite`);
}
const databaseName = (connection as Knex.ConnectionConfig)?.database;
@@ -205,6 +214,17 @@ export class DatabaseManager {
this.config.get('connection'),
this.config.getString('client'),
);
if (
client === 'sqlite3' &&
'filename' in baseConnection &&
baseConnection.filename !== ':memory:'
) {
throw new Error(
'`connection.filename` is not supported for the base sqlite connection. Prefer `connection.directory` or provide a filename for the plugin connection instead.',
);
}
// Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`. The
// `database` property from the base connection is omitted unless `pluginDivisionMode`
// is set to `schema`. SQLite3's `filename` property is an exception as this is used as a
@@ -73,28 +73,6 @@ describe('sqlite3', () => {
});
});
it('builds a persistent connection per database', () => {
expect(
buildSqliteDatabaseConfig(
createConfig({
filename: path.join('path', 'to', 'foo'),
}),
{
connection: {
database: 'my-database',
},
},
),
).toEqual({
client: 'sqlite3',
connection: {
filename: path.join('path', 'to', 'foo', 'my-database.sqlite'),
database: 'my-database',
},
useNullAsDefault: true,
});
});
it('replaces the connection with an override', () => {
expect(
buildSqliteDatabaseConfig(createConfig(':memory:'), {
@@ -86,19 +86,6 @@ export function buildSqliteDatabaseConfig(
overrides,
);
// If we don't create an in-memory database, interpret the connection string
// as a directory that contains multiple sqlite files based on the database
// name.
const database = (config.connection as Knex.ConnectionConfig).database;
const sqliteConnection = config.connection as Knex.Sqlite3ConnectionConfig;
if (database && sqliteConnection.filename !== ':memory:') {
sqliteConnection.filename = path.join(
sqliteConnection.filename,
`${database}.sqlite`,
);
}
return config;
}
+14
View File
@@ -1,5 +1,19 @@
# example-backend
## 0.2.57
### Patch Changes
- Updated dependencies
- @backstage/plugin-search-backend-module-elasticsearch@0.0.7
- @backstage/plugin-catalog-backend@0.19.2
- @backstage/plugin-scaffolder-backend@0.15.17
- @backstage/backend-common@0.9.14
- @backstage/plugin-azure-devops-backend@0.2.5
- @backstage/plugin-auth-backend@0.5.1
- @backstage/catalog-model@0.9.8
- example-app@0.2.57
## 0.2.56
### Patch Changes
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.2.56",
"version": "0.2.57",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -24,16 +24,16 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.9.13",
"@backstage/backend-common": "^0.9.14",
"@backstage/catalog-client": "^0.5.2",
"@backstage/catalog-model": "^0.9.7",
"@backstage/catalog-model": "^0.9.8",
"@backstage/config": "^0.1.10",
"@backstage/integration": "^0.6.10",
"@backstage/plugin-app-backend": "^0.3.19",
"@backstage/plugin-auth-backend": "^0.5.0",
"@backstage/plugin-azure-devops-backend": "^0.2.4",
"@backstage/plugin-auth-backend": "^0.5.1",
"@backstage/plugin-azure-devops-backend": "^0.2.5",
"@backstage/plugin-badges-backend": "^0.1.13",
"@backstage/plugin-catalog-backend": "^0.19.1",
"@backstage/plugin-catalog-backend": "^0.19.2",
"@backstage/plugin-code-coverage-backend": "^0.1.16",
"@backstage/plugin-graphql-backend": "^0.1.9",
"@backstage/plugin-jenkins-backend": "^0.1.9",
@@ -41,11 +41,11 @@
"@backstage/plugin-kafka-backend": "^0.2.12",
"@backstage/plugin-proxy-backend": "^0.2.14",
"@backstage/plugin-rollbar-backend": "^0.1.16",
"@backstage/plugin-scaffolder-backend": "^0.15.16",
"@backstage/plugin-scaffolder-backend": "^0.15.17",
"@backstage/plugin-scaffolder-backend-module-rails": "^0.2.0",
"@backstage/plugin-search-backend": "^0.2.8",
"@backstage/plugin-search-backend-node": "^0.4.2",
"@backstage/plugin-search-backend-module-elasticsearch": "^0.0.6",
"@backstage/plugin-search-backend-module-elasticsearch": "^0.0.7",
"@backstage/plugin-search-backend-module-pg": "^0.2.2",
"@backstage/plugin-techdocs-backend": "^0.12.0",
"@backstage/plugin-tech-insights-backend": "^0.1.3",
@@ -56,7 +56,7 @@
"@octokit/rest": "^18.5.3",
"azure-devops-node-api": "^11.0.1",
"dockerode": "^3.3.1",
"example-app": "^0.2.56",
"example-app": "^0.2.57",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-prom-bundle": "^6.3.6",
@@ -68,7 +68,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.10.1",
"@backstage/cli": "^0.10.2",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5"
+1
View File
@@ -89,6 +89,7 @@ async function main() {
argv: process.argv,
logger,
});
const createEnv = makeCreateEnv(config);
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
+9 -1
View File
@@ -71,7 +71,7 @@ export interface CatalogApi {
// @public
export class CatalogClient implements CatalogApi {
constructor(options: { discoveryApi: DiscoveryApi });
constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi });
addLocation(
{ type, target, dryRun, presence }: AddLocationRequest,
options?: CatalogRequestOptions,
@@ -121,6 +121,9 @@ export type CatalogEntitiesRequest = {
| Record<string, string | symbol | (string | symbol)[]>
| undefined;
fields?: string[] | undefined;
offset?: number;
limit?: number;
after?: string;
};
// @public
@@ -155,4 +158,9 @@ export type DiscoveryApi = {
// @public
export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE =
'backstage.io/catalog-processing';
// @public
export type FetchApi = {
fetch: typeof fetch;
};
```
@@ -175,6 +175,24 @@ describe('CatalogClient', () => {
{ apiVersion: '2' },
]);
});
it('builds paging parameters properly', async () => {
expect.assertions(2);
server.use(
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
expect(req.url.search).toBe('?offset=1&limit=2&after=%3D');
return res(ctx.json([]));
}),
);
const response = await client.getEntities(
{ offset: 1, limit: 2, after: '=' },
{ token },
);
expect(response.items).toEqual([]);
});
});
describe('getLocationById', () => {
+21 -8
View File
@@ -25,7 +25,7 @@ import {
stringifyLocationReference,
} from '@backstage/catalog-model';
import { ResponseError } from '@backstage/errors';
import fetch from 'cross-fetch';
import crossFetch from 'cross-fetch';
import {
CATALOG_FILTER_EXISTS,
AddLocationRequest,
@@ -38,6 +38,7 @@ import {
CatalogEntityAncestorsResponse,
} from './types/api';
import { DiscoveryApi } from './types/discovery';
import { FetchApi } from './types/fetch';
/**
* A frontend and backend compatible client for communicating with the Backstage Catalog.
@@ -46,9 +47,11 @@ import { DiscoveryApi } from './types/discovery';
* */
export class CatalogClient implements CatalogApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
constructor(options: { discoveryApi: DiscoveryApi }) {
constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi || { fetch: crossFetch };
}
/**
@@ -110,7 +113,7 @@ export class CatalogClient implements CatalogApi {
request?: CatalogEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<CatalogListResponse<Entity>> {
const { filter = [], fields = [] } = request ?? {};
const { filter = [], fields = [], offset, limit, after } = request ?? {};
const filterItems = [filter].flat();
const params: string[] = [];
@@ -141,6 +144,16 @@ export class CatalogClient implements CatalogApi {
params.push(`fields=${fields.map(encodeURIComponent).join(',')}`);
}
if (offset !== undefined) {
params.push(`offset=${offset}`);
}
if (limit !== undefined) {
params.push(`limit=${limit}`);
}
if (after !== undefined) {
params.push(`after=${encodeURIComponent(after)}`);
}
const query = params.length ? `?${params.join('&')}` : '';
const entities: Entity[] = await this.requestRequired(
'GET',
@@ -206,7 +219,7 @@ export class CatalogClient implements CatalogApi {
* @public
*/
async refreshEntity(entityRef: string, options?: CatalogRequestOptions) {
const response = await fetch(
const response = await this.fetchApi.fetch(
`${await this.discoveryApi.getBaseUrl('catalog')}/refresh`,
{
headers: {
@@ -237,7 +250,7 @@ export class CatalogClient implements CatalogApi {
{ type = 'url', target, dryRun, presence }: AddLocationRequest,
options?: CatalogRequestOptions,
): Promise<AddLocationResponse> {
const response = await fetch(
const response = await this.fetchApi.fetch(
`${await this.discoveryApi.getBaseUrl('catalog')}/locations${
dryRun ? '?dryRun=true' : ''
}`,
@@ -376,7 +389,7 @@ export class CatalogClient implements CatalogApi {
const headers: Record<string, string> = options?.token
? { Authorization: `Bearer ${options.token}` }
: {};
const response = await fetch(url, { method, headers });
const response = await this.fetchApi.fetch(url, { method, headers });
if (!response.ok) {
throw await ResponseError.fromResponse(response);
@@ -392,7 +405,7 @@ export class CatalogClient implements CatalogApi {
const headers: Record<string, string> = options?.token
? { Authorization: `Bearer ${options.token}` }
: {};
const response = await fetch(url, { method, headers });
const response = await this.fetchApi.fetch(url, { method, headers });
if (!response.ok) {
throw await ResponseError.fromResponse(response);
@@ -410,7 +423,7 @@ export class CatalogClient implements CatalogApi {
const headers: Record<string, string> = options?.token
? { Authorization: `Bearer ${options.token}` }
: {};
const response = await fetch(url, { method, headers });
const response = await this.fetchApi.fetch(url, { method, headers });
if (!response.ok) {
if (response.status === 404) {
+70
View File
@@ -29,11 +29,81 @@ export const CATALOG_FILTER_EXISTS = Symbol('CATALOG_FILTER_EXISTS');
* @public
*/
export type CatalogEntitiesRequest = {
/**
* If given, return only entities that match the given patterns.
*
* @remarks
*
* If multiple filter sets are given as an array, then there is effectively an
* OR between each filter set.
*
* Within one filter set, there is effectively an AND between the various
* keys.
*
* Within one key, if there are more than one value, then there is effectively
* an OR between them.
*
* Example: For an input of
*
* ```
* [
* { kind: ['API', 'Component'] },
* { 'metadata.name': 'a', 'metadata.namespace': 'b' }
* ]
* ```
*
* This effectively means
*
* ```
* (kind = EITHER 'API' OR 'Component')
* OR
* (metadata.name = 'a' AND metadata.namespace = 'b' )
* ```
*
* Each key is a dot separated path in each object.
*
* As a value you can also pass in the symbol `CATALOG_FILTER_EXISTS`
* (exported from this package), which means that you assert on the existence
* of that key, no matter what its value is.
*/
filter?:
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>
| undefined;
/**
* If given, return only the parts of each entity that match those dot
* separated paths in each object.
*
* @remarks
*
* Example: For an input of `['kind', 'metadata.annotations']`, then response
* objects will be shaped like
*
* ```
* {
* "kind": "Component",
* "metadata": {
* "annotations": {
* "foo": "bar"
* }
* }
* }
* ```
*/
fields?: string[] | undefined;
/**
* If given, skips over the first N items in the result set.
*/
offset?: number;
/**
* If given, returns at most N items from the result set.
*/
limit?: number;
/**
* If given, skips over all items before that cursor as returned by a previous
* request.
*/
after?: string;
};
/**
@@ -15,7 +15,7 @@
*/
/**
* This is a copy of the core DiscoveryApi, to avoid importing core.
* This is a copy of the DiscoveryApi, to avoid importing core-plugin-api.
*
* @public
*/
@@ -0,0 +1,24 @@
/*
* 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.
*/
/**
* This is a copy of FetchApi, to avoid importing core-plugin-api.
*
* @public
*/
export type FetchApi = {
fetch: typeof fetch;
};
@@ -25,5 +25,6 @@ export type {
CatalogEntityAncestorsResponse,
} from './api';
export type { DiscoveryApi } from './discovery';
export type { FetchApi } from './fetch';
export { CATALOG_FILTER_EXISTS } from './api';
export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status';
+6
View File
@@ -1,5 +1,11 @@
# @backstage/catalog-model
## 0.9.8
### Patch Changes
- ad7338bb48: Added an optional `presence` field to Location spec, which describes whether the target of a location is required to exist or not. It defaults to `'required'`, which is the current behaviour of the catalog.
## 0.9.7
### Patch Changes
+1
View File
@@ -325,6 +325,7 @@ interface LocationEntityV1alpha1 extends Entity {
type?: string;
target?: string;
targets?: string[];
presence?: 'required' | 'optional';
};
}
export { LocationEntityV1alpha1 as LocationEntity };
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/catalog-model",
"description": "Types and validators that help describe the model of a Backstage Catalog",
"version": "0.9.7",
"version": "0.9.8",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -42,7 +42,7 @@
"yup": "^0.32.9"
},
"devDependencies": {
"@backstage/cli": "^0.10.0",
"@backstage/cli": "^0.10.2",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
"yaml": "^1.9.2"
@@ -102,4 +102,23 @@ describe('LocationV1alpha1Validator', () => {
(entity as any).spec.targets = 7;
await expect(validator.check(entity)).rejects.toThrow(/targets/);
});
it('accepts good presence', async () => {
(entity as any).spec.presence = 'required';
await expect(validator.check(entity)).resolves.toBe(true);
(entity as any).spec.presence = 'optional';
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty presence', async () => {
(entity as any).spec.presence = '';
await expect(validator.check(entity)).rejects.toThrow(/presence/);
});
it('rejects wrong presence', async () => {
(entity as any).spec.presence = 7;
await expect(validator.check(entity)).rejects.toThrow(/presence/);
(entity as any).spec.presence = 'nope';
await expect(validator.check(entity)).rejects.toThrow(/presence/);
});
});
@@ -30,6 +30,7 @@ export interface LocationEntityV1alpha1 extends Entity {
type?: string;
target?: string;
targets?: string[];
presence?: 'required' | 'optional';
};
}
@@ -59,6 +59,13 @@
],
"minLength": 1
}
},
"presence": {
"type": "string",
"description": "Whether the presence of the location target is required and it should be considered an error if it can not be found",
"default": "required",
"examples": ["required"],
"enum": ["required", "optional"]
}
}
}
+12
View File
@@ -67,4 +67,16 @@ ignore:
reason: Prototype pollution is not an effective attack against a CLI as it already executes arbitrary code
expires: 2022-03-06T17:18:55.019Z
created: 2021-09-06T17:18:55.027Z
'snyk:lic:npm:rollup-plugin-dts:LGPL-3.0':
- '*':
reason: Backstage itself does not redistribute this dependency in minified form
expires: 2031-09-06T17:18:55.027Z
created: 2021-09-06T17:18:55.027Z
'snyk:lic:npm:axe-core:MPL-2.0':
- '*':
reason: Backstage itself does not redistribute this dependency in minified form
expires: 2031-09-06T17:18:55.027Z
created: 2021-09-06T17:18:55.027Z
patch: {}
+6
View File
@@ -1,5 +1,11 @@
# @backstage/cli
## 0.10.2
### Patch Changes
- 25dfc2d483: Add support for `.cjs` and `.mjs` extensions in local and dependency modules.
## 0.10.1
### Patch Changes
+5 -3
View File
@@ -96,20 +96,22 @@ async function getProjectConfig(targetPath, displayName) {
rootDir: path.resolve(targetPath, 'src'),
coverageDirectory: path.resolve(targetPath, 'coverage'),
coverageProvider: 'v8',
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'],
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'],
moduleNameMapper: {
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
},
transform: {
'\\.(js|jsx|ts|tsx)$': require.resolve('./jestSucraseTransform.js'),
'\\.(js|jsx|ts|tsx|mjs|cjs)$': require.resolve(
'./jestSucraseTransform.js',
),
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$':
require.resolve('./jestFileTransform.js'),
'\\.(yaml)$': require.resolve('jest-transform-yaml'),
},
// A bit more opinionated
testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'],
testMatch: ['**/?(*.)test.{js,jsx,ts,tsx,mjs,cjs}'],
transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`],
};
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.10.1",
"version": "0.10.2",
"private": false,
"publishConfig": {
"access": "public"
@@ -116,11 +116,11 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.9.13",
"@backstage/backend-common": "^0.9.14",
"@backstage/config": "^0.1.11",
"@backstage/core-components": "^0.8.0",
"@backstage/core-plugin-api": "^0.3.0",
"@backstage/core-app-api": "^0.2.0",
"@backstage/core-components": "^0.8.1",
"@backstage/core-plugin-api": "^0.3.1",
"@backstage/core-app-api": "^0.2.1",
"@backstage/dev-utils": "^0.2.14",
"@backstage/test-utils": "^0.1.24",
"@backstage/theme": "^0.2.14",
@@ -128,13 +128,13 @@
"@types/express": "^4.17.6",
"@types/fs-extra": "^9.0.1",
"@types/http-proxy": "^1.17.4",
"@types/inquirer": "^7.3.1",
"@types/inquirer": "^8.1.3",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.14.32",
"@types/recursive-readdir": "^2.2.0",
"@types/rollup-plugin-peer-deps-external": "^2.2.0",
"@types/rollup-plugin-postcss": "^2.0.0",
"@types/tar": "^4.0.3",
"@types/tar": "^6.1.1",
"@types/terser-webpack-plugin": "^5.0.4",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^3.11.5",
+1 -1
View File
@@ -20,7 +20,7 @@ import { paths } from '../lib/paths';
export default async (cmd: Command, cmdArgs: string[]) => {
const args = [
'--ext=js,jsx,ts,tsx',
'--ext=js,jsx,ts,tsx,mjs,cjs',
'--max-warnings=0',
`--format=${cmd.format}`,
...(cmdArgs ?? [paths.targetDir]),
+2 -2
View File
@@ -61,7 +61,7 @@ export const transforms = (options: TransformOptions): Transforms => {
},
},
{
test: /\.(jsx?|mjs)$/,
test: /\.(jsx?|mjs|cjs)$/,
exclude: /node_modules/,
loader: require.resolve('@sucrase/webpack-loader'),
options: {
@@ -71,7 +71,7 @@ export const transforms = (options: TransformOptions): Transforms => {
},
},
{
test: /\.m?js/,
test: /\.(js|mjs|cjs)/,
resolve: {
fullySpecified: false,
},
-1
View File
@@ -59,7 +59,6 @@ export async function loadCliConfig(options: Options) {
? async name => process.env[name] || 'x'
: undefined,
configRoot: paths.targetRoot,
configPaths: [],
configTargets: configTargets,
});
+9
View File
@@ -1,5 +1,14 @@
# @backstage/codemods
## 0.1.26
### Patch Changes
- Updated dependencies
- @backstage/core-app-api@0.2.1
- @backstage/core-plugin-api@0.3.1
- @backstage/core-components@0.8.1
## 0.1.25
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/codemods",
"description": "A collection of codemods for Backstage projects",
"version": "0.1.25",
"version": "0.1.26",
"private": false,
"publishConfig": {
"access": "public",
-5
View File
@@ -45,7 +45,6 @@ export function loadConfig(
// @public
export type LoadConfigOptions = {
configRoot: string;
configPaths: string[];
configTargets: ConfigTarget[];
env?: string;
experimentalEnvFunc?: (name: string) => Promise<string | undefined>;
@@ -103,8 +102,4 @@ export type TransformFunc<T extends number | string | boolean> = (
visibility: ConfigVisibility;
},
) => T | undefined;
// Warnings were encountered during analysis:
//
// src/loader.d.ts:33:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/config-loader" does not have an export "configTargets"
```
+4 -10
View File
@@ -118,7 +118,6 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [],
env: 'production',
}),
@@ -146,7 +145,6 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [{ url: configUrl }],
env: 'production',
remote: {
@@ -173,8 +171,10 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: ['/root/app-config2.yaml'],
configTargets: [{ path: '/root/app-config.yaml' }],
configTargets: [
{ path: '/root/app-config.yaml' },
{ path: '/root/app-config2.yaml' },
],
env: 'production',
}),
).resolves.toEqual({
@@ -207,7 +207,6 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: ['/root/app-config.yaml'],
configTargets: [{ path: '/root/app-config.yaml' }],
env: 'production',
}),
@@ -231,7 +230,6 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [
{ path: '/root/app-config.yaml' },
{ path: '/root/app-config.development.yaml' },
@@ -274,7 +272,6 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [{ path: '/root/app-config.substitute.yaml' }],
env: 'development',
}),
@@ -302,7 +299,6 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [],
watch: {
onChange: onChange.resolve,
@@ -353,7 +349,6 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [{ url: configUrl }],
watch: {
onChange: onChange.resolve,
@@ -401,7 +396,6 @@ describe('loadConfig', () => {
await loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [],
watch: {
onChange: () => {
+11 -15
View File
@@ -45,7 +45,7 @@ export type LoadConfigOptionsWatch = {
export type LoadConfigOptionsRemote = {
/**
* An optional remote config reloading period, in seconds
* A remote config reloading period, in seconds
*/
reloadIntervalSeconds: number;
};
@@ -59,11 +59,6 @@ export type LoadConfigOptions = {
// The root directory of the config loading context. Used to find default configs.
configRoot: string;
/** Absolute paths to load config files from. Configs from earlier paths have lower priority.
* @deprecated Use {@link configTargets} instead.
*/
configPaths: string[];
// Paths to load config files from. Configs from earlier paths have lower priority.
configTargets: ConfigTarget[];
@@ -114,20 +109,21 @@ export async function loadConfig(
.filter((e): e is { path: string } => e.hasOwnProperty('path'))
.map(configTarget => configTarget.path);
// Append deprecated configPaths to the absolute config paths received via configTargets.
options.configPaths.forEach(cp => {
if (!configPaths.includes(cp)) {
configPaths.push(cp);
}
});
const configUrls: string[] = options.configTargets
.slice()
.filter((e): e is { url: string } => e.hasOwnProperty('url'))
.map(configTarget => configTarget.url);
if (remote === undefined && configUrls.length > 0) {
throw new Error(`Remote config detected but this feature is turned off`);
if (remote === undefined) {
if (configUrls.length > 0) {
throw new Error(
`Please make sure you are passing the remote option when loading remote configurations. See https://backstage.io/docs/conf/writing#configuration-files for detailed info.`,
);
}
} else if (remote.reloadIntervalSeconds <= 0) {
throw new Error(
`Remote config must be contain a non zero reloadIntervalSeconds: <seconds> value`,
);
}
// If no paths are provided, we default to reading
+10
View File
@@ -1,5 +1,15 @@
# @backstage/core-app-api
## 0.2.1
### Patch Changes
- c11ce4f552: Deprecated `Auth0Auth`, pointing to using `OAuth2` directly instead.
- 9d6503e86c: Switched out usage of deprecated `OAuthRequestApi` types from `@backstage/core-plugin-api`.
- Updated dependencies
- @backstage/core-plugin-api@0.3.1
- @backstage/core-components@0.8.1
## 0.2.0
### Minor Changes
+38 -13
View File
@@ -17,15 +17,14 @@ import { AppTheme } from '@backstage/core-plugin-api';
import { AppThemeApi } from '@backstage/core-plugin-api';
import { atlassianAuthApiRef } from '@backstage/core-plugin-api';
import { auth0AuthApiRef } from '@backstage/core-plugin-api';
import { AuthProvider } from '@backstage/core-plugin-api';
import { AuthRequester } from '@backstage/core-plugin-api';
import { AuthRequesterOptions } from '@backstage/core-plugin-api';
import { AuthProviderInfo } from '@backstage/core-plugin-api';
import { AuthRequestOptions } from '@backstage/core-plugin-api';
import { BackstageIdentity } from '@backstage/core-plugin-api';
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { bitbucketAuthApiRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { Config } from '@backstage/config';
import { ConfigReader } from '@backstage/config';
import { createApp as createApp_2 } from '@backstage/app-defaults';
import { DiscoveryApi } from '@backstage/core-plugin-api';
@@ -36,6 +35,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { FeatureFlag } from '@backstage/core-plugin-api';
import { FeatureFlagsApi } from '@backstage/core-plugin-api';
import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
import { googleAuthApiRef } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
@@ -43,11 +43,13 @@ import { IdentityApi } from '@backstage/core-plugin-api';
import { microsoftAuthApiRef } from '@backstage/core-plugin-api';
import { OAuthApi } from '@backstage/core-plugin-api';
import { OAuthRequestApi } from '@backstage/core-plugin-api';
import { OAuthRequester } from '@backstage/core-plugin-api';
import { OAuthRequesterOptions } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { oktaAuthApiRef } from '@backstage/core-plugin-api';
import { oneloginAuthApiRef } from '@backstage/core-plugin-api';
import { OpenIdConnectApi } from '@backstage/core-plugin-api';
import { PendingAuthRequest } from '@backstage/core-plugin-api';
import { PendingOAuthRequest } from '@backstage/core-plugin-api';
import { PluginOutput } from '@backstage/core-plugin-api';
import { ProfileInfo } from '@backstage/core-plugin-api';
import { ProfileInfoApi } from '@backstage/core-plugin-api';
@@ -254,7 +256,7 @@ export class AtlassianAuth {
static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T;
}
// @public
// @public @deprecated
export class Auth0Auth {
// (undocumented)
static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T;
@@ -264,9 +266,7 @@ export class Auth0Auth {
export type AuthApiCreateOptions = {
discoveryApi: DiscoveryApi;
environment?: string;
provider?: AuthProvider & {
id: string;
};
provider?: AuthProviderInfo;
};
// @public
@@ -320,6 +320,12 @@ export function createApp(
options?: Parameters<typeof createApp_2>[0],
): BackstageApp & AppContext;
// @public
export function createFetchApi(options: {
baseImplementation?: typeof fetch | undefined;
middleware?: FetchMiddleware | FetchMiddleware[] | undefined;
}): FetchApi;
// @public
export function createSpecializedApp(options: AppOptions): BackstageApp;
@@ -371,6 +377,27 @@ export type FeatureFlaggedProps = {
}
);
// @public
export interface FetchMiddleware {
apply(next: typeof fetch): typeof fetch;
}
// @public
export class FetchMiddlewares {
static injectIdentityAuth(options: {
identityApi: IdentityApi;
config?: Config;
urlPrefixAllowlist?: string[];
header?: {
name: string;
value: (backstageToken: string) => string;
};
}): FetchMiddleware;
static resolvePluginProtocol(options: {
discoveryApi: DiscoveryApi;
}): FetchMiddleware;
}
// @public
export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null;
@@ -515,9 +542,9 @@ export type OAuthApiCreateOptions = AuthApiCreateOptions & {
// @public
export class OAuthRequestManager implements OAuthRequestApi {
// (undocumented)
authRequest$(): Observable<PendingAuthRequest[]>;
authRequest$(): Observable<PendingOAuthRequest[]>;
// (undocumented)
createAuthRequester<T>(options: AuthRequesterOptions<T>): AuthRequester<T>;
createAuthRequester<T>(options: OAuthRequesterOptions<T>): OAuthRequester<T>;
}
// @public
@@ -539,9 +566,7 @@ export type OneLoginAuthCreateOptions = {
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
provider?: AuthProvider & {
id: string;
};
provider?: AuthProviderInfo;
};
// @public
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-app-api",
"description": "Core app API used by Backstage apps",
"version": "0.2.0",
"version": "0.2.1",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,9 +30,9 @@
},
"dependencies": {
"@backstage/app-defaults": "^0.1.2",
"@backstage/core-components": "^0.8.0",
"@backstage/core-components": "^0.8.1",
"@backstage/config": "^0.1.11",
"@backstage/core-plugin-api": "^0.3.0",
"@backstage/core-plugin-api": "^0.3.1",
"@backstage/theme": "^0.2.14",
"@backstage/types": "^0.1.1",
"@backstage/version-bridge": "^0.1.1",
@@ -49,7 +49,7 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.1",
"@backstage/cli": "^0.10.2",
"@backstage/test-utils": "^0.1.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -0,0 +1,76 @@
/*
* 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.
*/
import { Config } from '@backstage/config';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware';
import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware';
import { FetchMiddleware } from './types';
/**
* A collection of common middlewares for the FetchApi.
*
* @public
*/
export class FetchMiddlewares {
/**
* Handles translation from `plugin://` URLs to concrete http(s) URLs based on
* the discovery API.
*
* @remarks
*
* If the request is for `plugin://catalog/entities?filter=x=y`, the discovery
* API will be queried for `'catalog'`. If it returned
* `https://backstage.example.net/api/catalog`, the resulting query would be
* `https://backstage.example.net/api/catalog/entities?filter=x=y`.
*
* If the incoming URL protocol was not `plugin`, the request is just passed
* through verbatim to the underlying implementation.
*/
static resolvePluginProtocol(options: {
discoveryApi: DiscoveryApi;
}): FetchMiddleware {
return new PluginProtocolResolverFetchMiddleware(options.discoveryApi);
}
/**
* Injects a Backstage token header when the user is signed in.
*
* @remarks
*
* Per default, an `Authorization: Bearer <token>` is generated. This can be
* customized using the `header` option.
*
* The header injection only happens on allowlisted URLs. Per default, if the
* `config` option is passed in, the `backend.baseUrl` is allowlisted, unless
* the `urlPrefixAllowlist` option is passed in, in which case it takes
* precedence. If you pass in neither config nor an allowlist, the middleware
* will have no effect.
*/
static injectIdentityAuth(options: {
identityApi: IdentityApi;
config?: Config;
urlPrefixAllowlist?: string[];
header?: {
name: string;
value: (backstageToken: string) => string;
};
}): FetchMiddleware {
return IdentityAuthInjectorFetchMiddleware.create(options);
}
private constructor() {}
}
@@ -0,0 +1,153 @@
/*
* 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.
*/
import { ConfigReader } from '@backstage/config';
import { IdentityApi } from '@backstage/core-plugin-api';
import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware';
describe('IdentityAuthInjectorFetchMiddleware', () => {
it('creates using defaults', async () => {
const middleware = IdentityAuthInjectorFetchMiddleware.create({
identityApi: undefined as any,
});
expect(middleware.urlPrefixAllowlist).toEqual([]);
expect(middleware.headerName).toEqual('authorization');
expect(middleware.headerValue('t')).toEqual('Bearer t');
});
it('creates using config', async () => {
const middleware = IdentityAuthInjectorFetchMiddleware.create({
identityApi: undefined as any,
config: new ConfigReader({
backend: { baseUrl: 'https://example.com/api' },
}),
header: { name: 'auth', value: t => `${t}!` },
});
expect(middleware.urlPrefixAllowlist).toEqual(['https://example.com/api']);
expect(middleware.headerName).toEqual('auth');
expect(middleware.headerValue('t')).toEqual('t!');
});
it('creates using explicit allowlist', async () => {
const middleware = IdentityAuthInjectorFetchMiddleware.create({
identityApi: undefined as any,
config: new ConfigReader({
backend: { baseUrl: 'https://example.com/api' },
}),
urlPrefixAllowlist: ['https://a.com', 'http://b.com:8080/'],
});
expect(middleware.urlPrefixAllowlist).toEqual([
'https://a.com',
'http://b.com:8080',
]);
});
it('injects the header only when a token is available', async () => {
const tokenFunction = jest.fn();
const identityApi = {
getCredentials: tokenFunction,
} as unknown as IdentityApi;
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
['https://example.com'],
'Authorization',
token => `Bearer ${token}`,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
// No token available
tokenFunction.mockResolvedValueOnce({ token: undefined });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]);
// Supply a token, header gets added
tokenFunction.mockResolvedValueOnce({ token: 'token' });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([
['authorization', 'Bearer token'],
]);
// Token no longer available
tokenFunction.mockResolvedValueOnce({ token: undefined });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]);
});
it('does not overwrite an existing header with the same name', async () => {
const identityApi = {
getCredentials: () => ({ token: 'token' }),
} as unknown as IdentityApi;
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
['https://example.com'],
'Authorization',
token => `Bearer ${token}`,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
// No token available
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([
['authorization', 'Bearer token'],
]);
// Supply a token, header gets added
await outer(
new Request('https://example.com', {
headers: { authorization: 'do-not-clobber' },
}),
);
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([
['authorization', 'do-not-clobber'],
]);
});
it('does not affect requests outside the allowlist', async () => {
const identityApi = {
getCredentials: () => ({ token: 'token' }),
} as unknown as IdentityApi;
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
['https://example.com:8080/root'],
'Authorization',
token => `Bearer ${token}`,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
await outer(new Request('https://example.com:8080/root'));
await outer(new Request('https://example.com:8080/root/sub'));
await outer(new Request('https://example.com:8080/root2'));
await outer(new Request('https://example.com/root'));
await outer(new Request('http://example.com:8080/root'));
await outer(new Request('https://example.com/root'));
const no: string[][] = [];
const yes: string[][] = [['authorization', 'Bearer token']];
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual(yes);
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual(yes);
expect([...inner.mock.calls[2][0].headers.entries()]).toEqual(no);
expect([...inner.mock.calls[3][0].headers.entries()]).toEqual(no);
expect([...inner.mock.calls[4][0].headers.entries()]).toEqual(no);
expect([...inner.mock.calls[5][0].headers.entries()]).toEqual(no);
});
});
@@ -0,0 +1,82 @@
/*
* 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.
*/
import { Config } from '@backstage/config';
import { IdentityApi } from '@backstage/core-plugin-api';
import { FetchMiddleware } from './types';
/**
* A fetch middleware, which injects a Backstage token header when the user is
* signed in.
*/
export class IdentityAuthInjectorFetchMiddleware implements FetchMiddleware {
static create(options: {
identityApi: IdentityApi;
config?: Config;
urlPrefixAllowlist?: string[];
header?: {
name: string;
value: (backstageToken: string) => string;
};
}): IdentityAuthInjectorFetchMiddleware {
const allowlist: string[] = [];
if (options.urlPrefixAllowlist) {
allowlist.push(...options.urlPrefixAllowlist);
} else if (options.config) {
allowlist.push(options.config.getString('backend.baseUrl'));
}
const headerName = options.header?.name || 'authorization';
const headerValue = options.header?.value || (token => `Bearer ${token}`);
return new IdentityAuthInjectorFetchMiddleware(
options.identityApi,
allowlist.map(prefix => prefix.replace(/\/$/, '')),
headerName,
headerValue,
);
}
constructor(
public readonly identityApi: IdentityApi,
public readonly urlPrefixAllowlist: string[],
public readonly headerName: string,
public readonly headerValue: (pluginId: string) => string,
) {}
apply(next: typeof fetch): typeof fetch {
return async (input, init) => {
// Skip this middleware if the header already exists, or if the URL
// doesn't match any of the allowlist items, or if there was no token
const request = new Request(input, init);
const { token } = await this.identityApi.getCredentials();
if (
request.headers.get(this.headerName) ||
!this.urlPrefixAllowlist.some(
prefix =>
request.url === prefix || request.url.startsWith(`${prefix}/`),
) ||
typeof token !== 'string' ||
!token
) {
return next(input, init);
}
request.headers.set(this.headerName, this.headerValue(token));
return next(request);
};
}
}
@@ -0,0 +1,93 @@
/*
* 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.
*/
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware';
describe('PluginProtocolResolverFetchMiddleware', () => {
it.each([['https://passthrough.com/a']])(
'passes through regular URLs, %p',
async url => {
const resolve = jest.fn();
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
const middleware = new PluginProtocolResolverFetchMiddleware(
discoveryApi,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
await outer(url);
expect(inner.mock.calls[0][0]).toBe(url);
expect(resolve).not.toBeCalled();
},
);
it.each([
[
'plugin://my-plugin/sub/path',
'my-plugin',
'https://real.com/base',
'https://real.com/base/sub/path',
],
[
'plugin://my-plugin/sub/path/',
'my-plugin',
'https://real.com/base/',
'https://real.com/base/sub/path/',
],
['plugin://x', 'x', 'http://real.com:8080', 'http://real.com:8080'],
[
'plugin://x/a/b?c=d&e=f#g',
'x',
'https://real.com/base',
'https://real.com/base/a/b?c=d&e=f#g',
],
[
'plugin://x?c=d&e=f#g',
'x',
'https://real.com:8080/base',
'https://real.com:8080/base?c=d&e=f#g',
],
[
'plugin://username:password@x?c=d&e=f#g',
'x',
'https://real.com:8080/base',
'https://username:password@real.com:8080/base?c=d&e=f#g',
],
[
'plugin://x?c=d&e=f#g',
'x',
'https://username:password@real.com:8080/base',
'https://username:password@real.com:8080/base?c=d&e=f#g',
],
])(
'resolves backstage URLs, %p',
async (original, host, resolved, result) => {
const resolve = jest.fn();
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
const middleware = new PluginProtocolResolverFetchMiddleware(
discoveryApi,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
resolve.mockResolvedValueOnce(resolved);
await outer(original);
expect(inner.mock.calls[0][0]).toBe(result);
expect(resolve).toHaveBeenLastCalledWith(host);
},
);
});
@@ -0,0 +1,61 @@
/*
* 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.
*/
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { FetchMiddleware } from './types';
function join(left: string, right: string): string {
if (!right || right === '/') {
return left;
}
return `${left.replace(/\/$/, '')}/${right.replace(/^\//, '')}`;
}
/**
* Handles translation from plugin://some-plugin-id/<path> to concrete http(s)
* URLs.
*/
export class PluginProtocolResolverFetchMiddleware implements FetchMiddleware {
constructor(private readonly discoveryApi: DiscoveryApi) {}
apply(next: typeof fetch): typeof fetch {
return async (input, init) => {
const request = new Request(input, init);
const prefix = 'plugin://';
if (!request.url.startsWith(prefix)) {
return next(input, init);
}
// Switch to a known protocol, since browser URL parsing misbehaves wildly
// on foreign protocols
const { hostname, pathname, search, hash, username, password } = new URL(
`http://${request.url.substring(prefix.length)}`,
);
let base = await this.discoveryApi.getBaseUrl(hostname);
if (username || password) {
const baseUrl = new URL(base);
const authority = `${username}${password ? `:${password}` : ''}@`;
base = `${baseUrl.protocol}//${authority}${baseUrl.host}${baseUrl.pathname}`;
}
const target = `${join(base, pathname)}${search}${hash}`;
return next(target, request);
};
}
}
@@ -0,0 +1,46 @@
/*
* 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.
*/
import { FetchApi } from '@backstage/core-plugin-api';
import { FetchMiddleware } from './types';
/**
* Builds a fetch API, based on the builtin fetch wrapped by a set of optional
* middleware implementations that add behaviors.
*
* @remarks
*
* The middleware are applied in reverse order, i.e. the last one will be
* "closest" to the base implementation. Passing in `[M1, M2, M3]` effectively
* leads to `M1(M2(M3(baseImplementation)))`.
*
* @public
*/
export function createFetchApi(options: {
baseImplementation?: typeof fetch | undefined;
middleware?: FetchMiddleware | FetchMiddleware[] | undefined;
}): FetchApi {
let result = options.baseImplementation || global.fetch;
const middleware = [options.middleware ?? []].flat().reverse();
for (const m of middleware) {
result = m.apply(result);
}
return {
fetch: result,
};
}
@@ -0,0 +1,19 @@
/*
* 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.
*/
export { createFetchApi } from './createFetchApi';
export { FetchMiddlewares } from './FetchMiddlewares';
export type { FetchMiddleware } from './types';
@@ -0,0 +1,30 @@
/*
* 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.
*/
/**
* A middleware that modifies the behavior of an ongoing fetch.
*
* @public
*/
export interface FetchMiddleware {
/**
* Applies this middleware to an inner implementation.
*
* @param next - The next, inner, implementation, that this middleware shall
* call out to as part of the request cycle.
*/
apply(next: typeof fetch): typeof fetch;
}
@@ -16,14 +16,14 @@
import {
OAuthRequestApi,
AuthRequesterOptions,
OAuthRequesterOptions,
} from '@backstage/core-plugin-api';
import { OAuthRequestManager } from './OAuthRequestManager';
export default class MockOAuthApi implements OAuthRequestApi {
private readonly real = new OAuthRequestManager();
createAuthRequester<T>(options: AuthRequesterOptions<T>) {
createAuthRequester<T>(options: OAuthRequesterOptions<T>) {
return this.real.createAuthRequester(options);
}
@@ -16,9 +16,9 @@
import {
OAuthRequestApi,
PendingAuthRequest,
AuthRequester,
AuthRequesterOptions,
PendingOAuthRequest,
OAuthRequester,
OAuthRequesterOptions,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests';
@@ -34,11 +34,17 @@ import { BehaviorSubject } from '../../../lib/subjects';
* @public
*/
export class OAuthRequestManager implements OAuthRequestApi {
private readonly subject = new BehaviorSubject<PendingAuthRequest[]>([]);
private currentRequests: PendingAuthRequest[] = [];
private readonly subject = new BehaviorSubject<PendingOAuthRequest[]>([]);
private currentRequests: PendingOAuthRequest[] = [];
private handlerCount = 0;
createAuthRequester<T>(options: AuthRequesterOptions<T>): AuthRequester<T> {
createAuthRequester<T>(options: OAuthRequesterOptions<T>): OAuthRequester<T> {
if (!options.provider.id) {
// eslint-disable-next-line no-console
console.warn(
'DEPRECATION WARNING: Not passing a provider id to createAuthRequester is deprecated, it will be required in the future',
);
}
const handler = new OAuthPendingRequests<T>();
const index = this.handlerCount;
@@ -67,8 +73,8 @@ export class OAuthRequestManager implements OAuthRequestApi {
// Converts the pending request and popup options into a popup request that we can forward to subscribers.
private makeAuthRequest(
request: PendingRequest<any>,
options: AuthRequesterOptions<any>,
): PendingAuthRequest | undefined {
options: OAuthRequesterOptions<any>,
): PendingOAuthRequest | undefined {
const { scopes } = request;
if (!scopes) {
return undefined;
@@ -88,7 +94,7 @@ export class OAuthRequestManager implements OAuthRequestApi {
};
}
authRequest$(): Observable<PendingAuthRequest[]> {
authRequest$(): Observable<PendingOAuthRequest[]> {
return this.subject;
}
}
@@ -28,6 +28,23 @@ const DEFAULT_PROVIDER = {
* Implements the OAuth flow to Auth0 products.
*
* @public
* @deprecated Use {@link OAuth2} instead
*
* @example
*
* ```ts
* OAuth2.create({
* discoveryApi,
* oauthRequestApi,
* provider: {
* id: 'auth0',
* title: 'Auth0',
* icon: () => null,
* },
* defaultScopes: ['openid', 'email', 'profile'],
* environment: configApi.getOptionalString('auth.environment'),
* })
* ```
*/
export default class Auth0Auth {
static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T {
@@ -17,7 +17,7 @@
import {
oneloginAuthApiRef,
OAuthRequestApi,
AuthProvider,
AuthProviderInfo,
DiscoveryApi,
} from '@backstage/core-plugin-api';
import { OAuth2 } from '../oauth2';
@@ -30,7 +30,7 @@ export type OneLoginAuthCreateOptions = {
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
provider?: AuthProvider & { id: string };
provider?: AuthProviderInfo;
};
const DEFAULT_PROVIDER = {
@@ -15,7 +15,7 @@
*/
import {
AuthProvider,
AuthProviderInfo,
DiscoveryApi,
OAuthRequestApi,
} from '@backstage/core-plugin-api';
@@ -36,5 +36,5 @@ export type OAuthApiCreateOptions = AuthApiCreateOptions & {
export type AuthApiCreateOptions = {
discoveryApi: DiscoveryApi;
environment?: string;
provider?: AuthProvider & { id: string };
provider?: AuthProviderInfo;
};
@@ -27,5 +27,6 @@ export * from './ConfigApi';
export * from './DiscoveryApi';
export * from './ErrorApi';
export * from './FeatureFlagsApi';
export * from './FetchApi';
export * from './OAuthRequestApi';
export * from './StorageApi';
@@ -15,9 +15,9 @@
*/
import {
AuthRequester,
OAuthRequester,
OAuthRequestApi,
AuthProvider,
AuthProviderInfo,
DiscoveryApi,
} from '@backstage/core-plugin-api';
import { showLoginPopup } from '../loginPopup';
@@ -36,7 +36,7 @@ type Options<AuthSession> = {
* Information about the auth provider to be shown to the user.
* The ID Must match the backend auth plugin configuration, for example 'google'.
*/
provider: AuthProvider & { id: string };
provider: AuthProviderInfo;
/**
* API used to instantiate an auth requester.
*/
@@ -65,9 +65,9 @@ export class DefaultAuthConnector<AuthSession>
{
private readonly discoveryApi: DiscoveryApi;
private readonly environment: string;
private readonly provider: AuthProvider & { id: string };
private readonly provider: AuthProviderInfo;
private readonly joinScopesFunc: (scopes: Set<string>) => string;
private readonly authRequester: AuthRequester<AuthSession>;
private readonly authRequester: OAuthRequester<AuthSession>;
private readonly sessionTransform: (response: any) => Promise<AuthSession>;
constructor(options: Options<AuthSession>) {
@@ -13,18 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthProvider, DiscoveryApi } from '@backstage/core-plugin-api';
import { AuthProviderInfo, DiscoveryApi } from '@backstage/core-plugin-api';
import { showLoginPopup } from '../loginPopup';
type Options = {
discoveryApi: DiscoveryApi;
environment?: string;
provider: AuthProvider & { id: string };
provider: AuthProviderInfo;
};
export class DirectAuthConnector<DirectAuthResponse> {
private readonly discoveryApi: DiscoveryApi;
private readonly environment: string | undefined;
private readonly provider: AuthProvider & { id: string };
private readonly provider: AuthProviderInfo;
constructor(options: Options) {
const { discoveryApi, environment, provider } = options;
+10
View File
@@ -1,5 +1,15 @@
# @backstage/core-components
## 0.8.1
### Patch Changes
- 2c17e5b073: Items in `<SidebarSubmenu>` are now only active when their full path is active (including search parameters).
- 9d6503e86c: Switched out usage of deprecated `OAuthRequestApi` types from `@backstage/core-plugin-api`.
- 1680a1c5ac: Add Missing Override Components Type for SidebarSpace, SidebarSpacer, and SidebarDivider Components.
- Updated dependencies
- @backstage/core-plugin-api@0.3.1
## 0.8.0
### Minor Changes
+24 -5
View File
@@ -814,13 +814,10 @@ export type ResponseErrorPanelClassKey = 'text' | 'divider';
export function RoutedTabs(props: { routes: SubRoute_2[] }): JSX.Element;
// Warning: (ae-forgotten-export) The symbol "SelectProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "SelectComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function Select(props: SelectProps): JSX.Element;
// Warning: (ae-missing-release-tag) "SelectClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type SelectClassKey =
| 'formControl'
@@ -830,11 +827,18 @@ export type SelectClassKey =
| 'checkbox'
| 'root';
// Warning: (ae-missing-release-tag) "SelectInputBaseClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type SelectedItems = string | string[] | number | number[];
// @public (undocumented)
export type SelectInputBaseClassKey = 'root' | 'input';
// @public (undocumented)
export type SelectItem = {
label: string;
value: string | number;
};
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "Sidebar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -1152,6 +1156,11 @@ export const SidebarDivider: React_2.ComponentType<
}
>;
// Warning: (ae-missing-release-tag) "SidebarDividerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type SidebarDividerClassKey = 'root';
// @public
export const SidebarExpandButton: () => JSX.Element | null;
@@ -1760,6 +1769,11 @@ export const SidebarSpace: React_2.ComponentType<
}
>;
// Warning: (ae-missing-release-tag) "SidebarSpaceClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type SidebarSpaceClassKey = 'root';
// Warning: (ae-missing-release-tag) "SidebarSpacer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -2029,6 +2043,11 @@ export const SidebarSpacer: React_2.ComponentType<
}
>;
// Warning: (ae-missing-release-tag) "SidebarSpacerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type SidebarSpacerClassKey = 'root';
// @public
export const SidebarSubmenu: (props: SidebarSubmenuProps) => JSX.Element;
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
"version": "0.8.0",
"version": "0.8.1",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.11",
"@backstage/core-plugin-api": "^0.3.0",
"@backstage/core-plugin-api": "^0.3.1",
"@backstage/errors": "^0.1.5",
"@backstage/theme": "^0.2.14",
"@material-table/core": "^3.1.0",
@@ -72,8 +72,8 @@
"react-dom": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/core-app-api": "^0.2.0",
"@backstage/cli": "^0.10.1",
"@backstage/core-app-api": "^0.2.1",
"@backstage/cli": "^0.10.2",
"@backstage/test-utils": "^0.1.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -22,7 +22,7 @@ import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import React, { useState } from 'react';
import { isError } from '@backstage/errors';
import { PendingAuthRequest } from '@backstage/core-plugin-api';
import { PendingOAuthRequest } from '@backstage/core-plugin-api';
export type LoginRequestListItemClassKey = 'root';
@@ -36,7 +36,7 @@ const useItemStyles = makeStyles<Theme>(
);
type RowProps = {
request: PendingAuthRequest;
request: PendingOAuthRequest;
busy: boolean;
setBusy: (busy: boolean) => void;
};

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