@@ -1,7 +0,0 @@
|
||||
---
|
||||
id: backend
|
||||
title: Backend
|
||||
description: About Backend
|
||||
---
|
||||
|
||||
## TODO
|
||||
@@ -13,31 +13,31 @@ both with other plugins and the app itself.
|
||||
|
||||
Backstage provides two primary methods for plugins to communicate across their
|
||||
boundaries in client-side code. The first one being the
|
||||
[createPlugin](../reference/core-plugin-api.createplugin.md) API along with the
|
||||
[`createPlugin`](../reference/core-plugin-api.createplugin.md) API along with the
|
||||
extensions that it can provide, and the second one being Utility APIs. While the
|
||||
[createPlugin](../reference/core-plugin-api.createplugin.md) API is focused on
|
||||
[`createPlugin`](../reference/core-plugin-api.createplugin.md) API is focused on
|
||||
the initialization plugins and the app, the Utility APIs provide ways for
|
||||
plugins to communicate during their entire life cycle.
|
||||
|
||||
## Consuming APIs
|
||||
|
||||
Each Utility API is tied to an [ApiRef](../reference/core-plugin-api.apiref.md)
|
||||
Each Utility API is tied to an [`ApiRef`](../reference/core-plugin-api.apiref.md)
|
||||
instance, which is a global singleton object without any additional state or
|
||||
functionality, its only purpose is to reference Utility APIs.
|
||||
[ApiRef](../reference/core-plugin-api.apiref.md)s are created using
|
||||
[createApiRef](../reference/core-plugin-api.createapiref.md), which is exported
|
||||
by [@backstage/core-plugin-api](../reference/core-plugin-api.md). There are also
|
||||
[`ApiRef`](../reference/core-plugin-api.apiref.md)s are created using
|
||||
[`createApiRef`](../reference/core-plugin-api.createapiref.md), which is exported
|
||||
by [`@backstage/core-plugin-api`](../reference/core-plugin-api.md). There are also
|
||||
many predefined Utility APIs in
|
||||
[@backstage/core-plugin-api](../reference/core-plugin-api.md), and they're all
|
||||
[`@backstage/core-plugin-api`](../reference/core-plugin-api.md), and they're all
|
||||
exported with a name of the pattern `*ApiRef`, for example
|
||||
[errorApiRef](../reference/core-plugin-api.errorapiref.md).
|
||||
[`errorApiRef`](../reference/core-plugin-api.errorapiref.md).
|
||||
|
||||
To access one of the Utility APIs inside a React component, use the
|
||||
[useApi](../reference/core-plugin-api.useapi.md) hook exported by
|
||||
[@backstage/core-plugin-api](../reference/core-plugin-api.md), or the
|
||||
[withApis](../reference/core-plugin-api.withapis.md) HOC if you prefer class
|
||||
[`useApi`](../reference/core-plugin-api.useapi.md) hook exported by
|
||||
[`@backstage/core-plugin-api`](../reference/core-plugin-api.md), or the
|
||||
[`withApis`](../reference/core-plugin-api.withapis.md) HOC if you prefer class
|
||||
components. For example, the
|
||||
[ErrorApi](../reference/core-plugin-api.errorapi.md) can be accessed like this:
|
||||
[`ErrorApi`](../reference/core-plugin-api.errorapi.md) can be accessed like this:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
@@ -56,14 +56,14 @@ export const MyComponent = () => {
|
||||
```
|
||||
|
||||
Note that there is no explicit type given for
|
||||
[ErrorApi](../reference/core-plugin-api.errorapi.md). This is because the
|
||||
[errorApiRef](../reference/core-plugin-api.errorapiref.md) has the type
|
||||
embedded, and [useApi](../reference/core-plugin-api.useapi.md) is able to infer
|
||||
[`ErrorApi`](../reference/core-plugin-api.errorapi.md). This is because the
|
||||
[`errorApiRef`](../reference/core-plugin-api.errorapiref.md) has the type
|
||||
embedded, and [`useApi`](../reference/core-plugin-api.useapi.md) is able to infer
|
||||
the type.
|
||||
|
||||
Also note that consuming Utility APIs is not limited to plugins, it can be done
|
||||
from any component inside Backstage, including the ones in
|
||||
[@backstage/core-plugin-api](../reference/core-plugin-api.md). The only
|
||||
[`@backstage/core-plugin-api`](../reference/core-plugin-api.md). The only
|
||||
requirement is that they are beneath the `AppProvider` in the react tree.
|
||||
|
||||
## Supplying APIs
|
||||
@@ -71,15 +71,15 @@ requirement is that they are beneath the `AppProvider` in the react tree.
|
||||
### API Factories
|
||||
|
||||
APIs are registered in the form of
|
||||
[ApiFactories](../reference/core-plugin-api.apifactory.md), which encapsulate
|
||||
[`ApiFactory`](../reference/core-plugin-api.apifactory.md) instances, which encapsulate
|
||||
the process of instantiating an API. It is a collection of three things: the
|
||||
[ApiRef](../reference/core-plugin-api.apiref.md) of the API to instantiate, a
|
||||
[`ApiRef`](../reference/core-plugin-api.apiref.md) of the API to instantiate, a
|
||||
list of all required dependencies, and a factory function that returns a new API
|
||||
instance.
|
||||
|
||||
For example, this is the default
|
||||
[ApiFactory](../reference/core-plugin-api.apifactory.md) for the
|
||||
[ErrorApi](../reference/core-plugin-api.errorapi.md):
|
||||
[`ApiFactory`](../reference/core-plugin-api.apifactory.md) for the
|
||||
[`ErrorApi`](../reference/core-plugin-api.errorapi.md):
|
||||
|
||||
```ts
|
||||
createApiFactory({
|
||||
@@ -93,25 +93,25 @@ createApiFactory({
|
||||
});
|
||||
```
|
||||
|
||||
In this example the [errorApiRef](../reference/core-plugin-api.errorapiref.md)
|
||||
In this example the [`errorApiRef`](../reference/core-plugin-api.errorapiref.md)
|
||||
is our API, which encapsulates the
|
||||
[ErrorApi](../reference/core-plugin-api.errorapi.md) type. The
|
||||
[alertApiRef](../reference/core-plugin-api.alertapiref.md) is our single
|
||||
[`ErrorApi`](../reference/core-plugin-api.errorapi.md) type. The
|
||||
[`alertApiRef`](../reference/core-plugin-api.alertapiref.md) is our single
|
||||
dependency, which we give the name `alertApi`, and is then passed on to the
|
||||
factory function, which returns an implementation of the
|
||||
[ErrorApi](../reference/core-plugin-api.errorapi.md).
|
||||
[`ErrorApi`](../reference/core-plugin-api.errorapi.md).
|
||||
|
||||
The [createApiFactory](../reference/core-plugin-api.createapifactory.md)
|
||||
The [`createApiFactory`](../reference/core-plugin-api.createapifactory.md)
|
||||
function is a thin wrapper that enables TypeScript type inference. You may
|
||||
notice that there are no type annotations in the above example, and that is
|
||||
because we're able to infer all types from the
|
||||
[ApiRef](../reference/core-plugin-api.apiref.md)s. TypeScript will make sure
|
||||
[`ApiRef`](../reference/core-plugin-api.apiref.md)s. TypeScript will make sure
|
||||
that the return value of the `factory` function matches the type embedded in
|
||||
`api`'s [ApiRef](../reference/core-plugin-api.apiref.md), in this case the
|
||||
[ErrorApi](../reference/core-plugin-api.errorapi.md). It will also match the
|
||||
`api`'s [`ApiRef`](../reference/core-plugin-api.apiref.md), in this case the
|
||||
[`ErrorApi`](../reference/core-plugin-api.errorapi.md). It will also match the
|
||||
types between the `deps` and the parameters of the `factory` function, again
|
||||
using the type embedded within the
|
||||
[ApiRef](../reference/core-plugin-api.apiref.md)s.
|
||||
[`ApiRef`](../reference/core-plugin-api.apiref.md)s.
|
||||
|
||||
## Registering API Factories
|
||||
|
||||
@@ -123,13 +123,13 @@ app, and the app itself.
|
||||
|
||||
Starting with the Backstage core library, it provides implementations for all of
|
||||
the core APIs. The core APIs are the ones exported by
|
||||
[@backstage/core-plugin-api](../reference/core-plugin-api.md), such as the
|
||||
[errorApiRef](../reference/core-plugin-api.errorapiref.md) and
|
||||
[configApiRef](../reference/core-plugin-api.configapiref.md).
|
||||
[`@backstage/core-plugin-api`](../reference/core-plugin-api.md), such as the
|
||||
[`errorApiRef`](../reference/core-plugin-api.errorapiref.md) and
|
||||
[`configApiRef`](../reference/core-plugin-api.configapiref.md).
|
||||
|
||||
The core APIs are loaded for any app created with
|
||||
[createApp](../reference/app-defaults.createapp.md) from
|
||||
[@backstage/core-plugin-api](../reference/app-defaults.md), which means that
|
||||
[`createApp`](../reference/app-defaults.createapp.md) from
|
||||
[`@backstage/core-plugin-api`](../reference/app-defaults.md), which means that
|
||||
there is no step that needs to be taken to include these APIs in an app.
|
||||
|
||||
### Plugin APIs
|
||||
@@ -137,13 +137,13 @@ there is no step that needs to be taken to include these APIs in an app.
|
||||
In addition to the core APIs, plugins can define and export their own APIs.
|
||||
While doing so they should usually also provide default implementations of their
|
||||
own APIs, for example, the `catalog` plugin exports `catalogApiRef`, and also
|
||||
supplies a default [ApiFactory](../reference/core-plugin-api.apifactory.md) of
|
||||
supplies a default [`ApiFactory`](../reference/core-plugin-api.apifactory.md) of
|
||||
that API using the `CatalogClient`. There is one restriction to plugin-provided
|
||||
API Factories: plugins may not supply factories for core APIs, trying to do so
|
||||
will cause the app to refuse to start.
|
||||
|
||||
Plugins supply their APIs through the `apis` option of
|
||||
[createPlugin](../reference/core-plugin-api.createplugin.md), for example:
|
||||
[`createPlugin`](../reference/core-plugin-api.createplugin.md), for example:
|
||||
|
||||
```ts
|
||||
export const techdocsPlugin = createPlugin({
|
||||
@@ -168,7 +168,7 @@ Lastly, the app itself is the final point where APIs can be added, and what has
|
||||
the final say in what APIs will be loaded at runtime. The app may override the
|
||||
factories for any of the core or plugin APIs, with the exception of the config,
|
||||
app theme, and identity APIs. These are static APIs that are tied into the
|
||||
[createApp](../reference/app-defaults.createapp.md) implementation, and
|
||||
[`createApp`](../reference/app-defaults.createapp.md) implementation, and
|
||||
therefore not possible to override.
|
||||
|
||||
Overriding APIs is useful for apps that want to switch out behavior to tailor it
|
||||
@@ -231,19 +231,19 @@ const app = createApp({
|
||||
```
|
||||
|
||||
Note that the above line will cause an error if `IgnoreErrorApi` does not fully
|
||||
implement the [ErrorApi](../reference/core-plugin-api.errorapi.md), as it is
|
||||
implement the [`ErrorApi`](../reference/core-plugin-api.errorapi.md), as it is
|
||||
checked by the type embedded in the
|
||||
[errorApiRef](../reference/core-plugin-api.errorapiref.md) at compile time.
|
||||
[`errorApiRef`](../reference/core-plugin-api.errorapiref.md) at compile time.
|
||||
|
||||
## Defining custom Utility APIs
|
||||
|
||||
Plugins are free to define their own Utility APIs. Simply define the TypeScript
|
||||
interface for the API, and create an
|
||||
[ApiRef](../reference/core-plugin-api.apiref.md) using
|
||||
[createApiRef](../reference/core-plugin-api.createapiref.md) exported from
|
||||
[@backstage/core-plugin-api](../reference/core-plugin-api.md). Also be sure to
|
||||
[`ApiRef`](../reference/core-plugin-api.apiref.md) using
|
||||
[`createApiRef`](../reference/core-plugin-api.createapiref.md) exported from
|
||||
[`@backstage/core-plugin-api`](../reference/core-plugin-api.md). Also be sure to
|
||||
provide at least one implementation of the API, and to declare a default factory
|
||||
for the API in [createPlugin](../reference/core-plugin-api.createplugin.md).
|
||||
for the API in [`createPlugin`](../reference/core-plugin-api.createplugin.md).
|
||||
|
||||
Custom Utility APIs can be either public or private, which is up to the plugin
|
||||
to choose. Private APIs do not expose an external API surface, and it's
|
||||
@@ -255,16 +255,16 @@ backwards compatibility of public APIs, as you may otherwise break apps that are
|
||||
using your plugin.
|
||||
|
||||
To make an API public, simply export the
|
||||
[ApiRef](../reference/core-plugin-api.apiref.md) of the API, and any associated
|
||||
[`ApiRef`](../reference/core-plugin-api.apiref.md) of the API, and any associated
|
||||
types. To make an API private, just avoid exporting the
|
||||
[ApiRef](../reference/core-plugin-api.apiref.md), but still be sure to supply a
|
||||
default factory to [createPlugin](../reference/core-plugin-api.createplugin.md).
|
||||
[`ApiRef`](../reference/core-plugin-api.apiref.md), but still be sure to supply a
|
||||
default factory to [`createPlugin`](../reference/core-plugin-api.createplugin.md).
|
||||
|
||||
Private APIs are useful for plugins that want to depend on other APIs outside of
|
||||
React components, but not have to expose an entire API surface to maintain. When
|
||||
using private APIs, it is fine to use the `typeof` of an implementing class as
|
||||
the type parameter passed to
|
||||
[createApiRef](../reference/core-plugin-api.createapiref.md), while public APIs
|
||||
[`createApiRef`](../reference/core-plugin-api.createapiref.md), while public APIs
|
||||
should always define a separate TypeScript interface type.
|
||||
|
||||
Plugins may depend on APIs from other plugins, both in React components and as
|
||||
@@ -273,13 +273,13 @@ dependencies between plugins.
|
||||
|
||||
## Architecture
|
||||
|
||||
The [ApiRef](../reference/core-plugin-api.apiref.md) instances mentioned above
|
||||
The [`ApiRef`](../reference/core-plugin-api.apiref.md) instances mentioned above
|
||||
provide a point of indirection between consumers and producers of Utility APIs.
|
||||
It allows for plugins and components to depend on APIs in a type-safe way,
|
||||
without having a direct reference to a concrete implementation of the APIs. The
|
||||
Apps are also given a lot of flexibility in what implementations to provide. As
|
||||
long as they adhere to the contract established by an
|
||||
[ApiRef](../reference/core-plugin-api.apiref.md), they are free to choose any
|
||||
[`ApiRef`](../reference/core-plugin-api.apiref.md), they are free to choose any
|
||||
implementation they want.
|
||||
|
||||
The figure below shows the relationship between
|
||||
@@ -304,16 +304,16 @@ The indirection provided by Utility APIs also makes it straightforward to test
|
||||
components that depend on APIs, and to provide a standard common development
|
||||
environment for plugins. A proper test wrapper with mocked API implementations
|
||||
is not yet ready, but it will be provided as a part of
|
||||
[@backstage/test-utils](../reference/test-utils.md). It will provide mocked
|
||||
[`@backstage/test-utils`](../reference/test-utils.md). It will provide mocked
|
||||
variants of APIs, with additional methods for asserting a component's
|
||||
interaction with the API.
|
||||
|
||||
The common development environment for plugins is included in
|
||||
[@backstage/dev-utils](../reference/dev-utils.md), where the exported
|
||||
[createDevApp](../reference/dev-utils.createdevapp.md) function creates an
|
||||
[`@backstage/dev-utils`](../reference/dev-utils.md), where the exported
|
||||
[`createDevApp`](../reference/dev-utils.createdevapp.md) function creates an
|
||||
application with implementations for all core APIs already present. Contrary to
|
||||
the method for wiring up Utility API implementations in an app created with
|
||||
[createApp](../reference/app-defaults.createapp.md),
|
||||
[createDevApp](../reference/dev-utils.createdevapp.md) uses automatic dependency
|
||||
[`createApp`](../reference/app-defaults.createapp.md),
|
||||
[`createDevApp`](../reference/dev-utils.createdevapp.md) uses automatic dependency
|
||||
injection. This is to make it possible to replace any API implementation, and
|
||||
having that be reflected in dependents of that API.
|
||||
|
||||
@@ -15,7 +15,7 @@ thing well". The module would be consumed
|
||||
(`const localName = require('the-module');`) without having to know the internal
|
||||
structure.
|
||||
|
||||
Now, ESModules are the primary authoring format. They have numerous benefits,
|
||||
Now, `ESModules` are the primary authoring format. They have numerous benefits,
|
||||
such as compile-time verification of exports, and standards-defined semantics.
|
||||
They have a similar mechanism known as "default exports", which allows for a
|
||||
consumer to `import localName from 'the-module';`. This is implicitly the same
|
||||
|
||||
@@ -34,5 +34,5 @@ Records should be stored under the `architecture-decisions` directory.
|
||||
|
||||
## Superseding an ADR
|
||||
|
||||
If an ADR supersedes an older ADR then the older ADR's status is changed to
|
||||
superseded by ADR-XXXX and links to the new ADR.
|
||||
If an ADR supersedes an older ADR then the status of the older ADR is changed to
|
||||
"superseded by ADR-XXXX", and links to the new ADR.
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 40 KiB |
@@ -18,7 +18,7 @@ Settings for local development:
|
||||
|
||||
- Name: Backstage (or your custom app name)
|
||||
- Redirect URI: `http://localhost:7007/api/auth/gitlab/handler/frame`
|
||||
- Scopes: read_user
|
||||
- Scopes: `read_user`
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -35,6 +35,8 @@ auth:
|
||||
clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
|
||||
## uncomment if using self-hosted GitLab
|
||||
# audience: https://gitlab.company.com
|
||||
## uncomment if using a custom redirect URI
|
||||
# callbackUrl: https://${BASE_URL}/api/auth/gitlab/handler/frame
|
||||
```
|
||||
|
||||
The GitLab provider is a structure with three configuration keys:
|
||||
@@ -44,6 +46,9 @@ The GitLab provider is a structure with three configuration keys:
|
||||
- `clientSecret`: The Application secret
|
||||
- `audience` (optional): The base URL for the self-hosted GitLab instance, e.g.
|
||||
`https://gitlab.company.com`
|
||||
- `callbackUrl` (optional): The URL matching the Redirect URI registered when creating your GitLab OAuth App, e.g.
|
||||
`https://$backstage.acme.corp/api/auth/gitlab/handler/frame`
|
||||
Note: Due to a peculiarity with GitLab OAuth, ensure there is no trailing `/` after 'frame' in the URL.
|
||||
|
||||
## Adding the provider to the Backstage frontend
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ The Microsoft provider is a structure with three configuration keys:
|
||||
- `clientSecret`: Secret, found on App Registration > Certificates & secrets
|
||||
- `tenantId`: Directory (tenant) ID, found on App Registration > Overview
|
||||
|
||||
In order to finish signing a user in from Azure, the Backstage backend must
|
||||
fetch their information from graph.microsoft.com (as seen in [this source
|
||||
code](https://github.com/seanfisher/passport-microsoft/blob/0456aa9bce05579c18e77f51330176eb26373658/lib/strategy.js#L93-L95)),
|
||||
so ensure that your Backstage backend has connectivity to this host.
|
||||
Otherwise users may see an `Authentication failed, failed to fetch user profile` error when they attempt to log in.
|
||||
|
||||
## Adding the provider to the Backstage frontend
|
||||
|
||||
To add the provider to the frontend, add the `microsoftAuthApiRef` 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 [Sign In Configuration][3] docs, here's where you import
|
||||
and use the API reference 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
|
||||
@@ -23,6 +23,17 @@ Each package is searched for a schema at a single point of entry, a top-level
|
||||
inlined JSON schema, or a relative path to a schema file. Supported schema file
|
||||
formats are `.json` or `.d.ts`.
|
||||
|
||||
```jsonc title="package.json"
|
||||
{
|
||||
// ...
|
||||
"files": [
|
||||
// ...
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
```
|
||||
|
||||
> When defining a schema file, be sure to include the file in your
|
||||
> `package.json` > `"files"` field as well!
|
||||
|
||||
@@ -127,7 +138,7 @@ may need to pass in all files using one or multiple `--config <path>` options.
|
||||
> to change for different deployment environments should be static
|
||||
> configuration, while it should otherwise be avoided.
|
||||
|
||||
When defining configuration for your plugin, keep keys camelCased and stick to
|
||||
When defining configuration for your plugin, keep keys on `camelCase` form and stick to
|
||||
existing casing conventions such as `baseUrl` rather than `baseURL`.
|
||||
|
||||
It is also usually best to prefer objects over arrays, as it makes it possible
|
||||
|
||||
@@ -61,13 +61,18 @@ FROM node:16-bullseye-slim
|
||||
|
||||
# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image,
|
||||
# in which case you should also move better-sqlite3 to "devDependencies" in package.json.
|
||||
RUN apt-get update && \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
yarn config set python /usr/bin/python3
|
||||
|
||||
# From here on we use the least-privileged `node` user to run the backend.
|
||||
USER node
|
||||
|
||||
# This should create the app dir as `node`.
|
||||
# If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`.
|
||||
# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`.
|
||||
WORKDIR /app
|
||||
|
||||
# This switches many Node.js dependencies to production mode.
|
||||
@@ -79,7 +84,8 @@ ENV NODE_ENV production
|
||||
COPY --chown=node:node yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
|
||||
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
|
||||
|
||||
RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)"
|
||||
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
|
||||
yarn install --frozen-lockfile --production --network-timeout 300000
|
||||
|
||||
# Then copy the rest of the backend bundle, along with any other files we might want.
|
||||
COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./
|
||||
@@ -163,53 +169,68 @@ RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {
|
||||
# Stage 2 - Install dependencies and build packages
|
||||
FROM node:16-bullseye-slim AS build
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=packages /app .
|
||||
|
||||
# install sqlite3 dependencies
|
||||
RUN apt-get update && \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \
|
||||
yarn config set python /usr/bin/python3
|
||||
|
||||
RUN yarn install --frozen-lockfile --network-timeout 600000 && rm -rf "$(yarn cache dir)"
|
||||
USER node
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
COPY --from=packages --chown=node:node /app .
|
||||
|
||||
# Stop cypress from downloading it's massive binary.
|
||||
ENV CYPRESS_INSTALL_BINARY=0
|
||||
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
|
||||
yarn install --frozen-lockfile --network-timeout 600000
|
||||
|
||||
COPY --chown=node:node . .
|
||||
|
||||
RUN yarn tsc
|
||||
RUN yarn --cwd packages/backend build
|
||||
# If you have not yet migrated to package roles, use the following command instead:
|
||||
# RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies
|
||||
|
||||
RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \
|
||||
&& tar xzf packages/backend/dist/skeleton.tar.gz -C packages/backend/dist/skeleton \
|
||||
&& tar xzf packages/backend/dist/bundle.tar.gz -C packages/backend/dist/bundle
|
||||
|
||||
# Stage 3 - Build the actual backend image and install production dependencies
|
||||
FROM node:16-bullseye-slim
|
||||
|
||||
# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image,
|
||||
# in which case you should also move better-sqlite3 to "devDependencies" in package.json.
|
||||
RUN apt-get update && \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
yarn config set python /usr/bin/python3
|
||||
|
||||
# From here on we use the least-privileged `node` user to run the backend.
|
||||
USER node
|
||||
|
||||
# This should create the app dir as `node`.
|
||||
# If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`.
|
||||
# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`.
|
||||
WORKDIR /app
|
||||
|
||||
# This switches many Node.js dependencies to production mode.
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Copy the install dependencies from the build stage and context
|
||||
COPY --from=build --chown=node:node /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./
|
||||
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
|
||||
COPY --from=build --chown=node:node /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton/ ./
|
||||
|
||||
RUN yarn install --frozen-lockfile --production --network-timeout 600000 && rm -rf "$(yarn cache dir)"
|
||||
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
|
||||
yarn install --frozen-lockfile --production --network-timeout 600000
|
||||
|
||||
# Copy the built packages from the build stage
|
||||
COPY --from=build --chown=node:node /app/packages/backend/dist/bundle.tar.gz .
|
||||
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
|
||||
COPY --from=build --chown=node:node /app/packages/backend/dist/bundle/ ./
|
||||
|
||||
# Copy any other files that we need at runtime
|
||||
COPY --chown=node:node app-config.yaml ./
|
||||
|
||||
# This switches many Node.js dependencies to production mode.
|
||||
ENV NODE_ENV production
|
||||
|
||||
CMD ["node", "packages/backend", "--config", "app-config.yaml"]
|
||||
```
|
||||
|
||||
@@ -225,6 +246,7 @@ however ignore any existing build output or dependencies on the host. For our
|
||||
new `.dockerignore`, replace the contents of your existing one with this:
|
||||
|
||||
```text
|
||||
dist-types
|
||||
node_modules
|
||||
packages/*/dist
|
||||
packages/*/node_modules
|
||||
|
||||
@@ -62,7 +62,7 @@ This is an array used to determine where to retrieve cluster configuration from.
|
||||
Valid cluster locator methods are:
|
||||
|
||||
- [`catalog`](#catalog)
|
||||
- [`localKubectlProxy`](#localKubectlProxy)
|
||||
- [`localKubectlProxy`](#localkubectlproxy)
|
||||
- [`config`](#config)
|
||||
- [`gke`](#gke)
|
||||
- [custom `KubernetesClustersSupplier`](#custom-kubernetesclusterssupplier)
|
||||
@@ -217,11 +217,11 @@ for some dashboards, such as GKE.
|
||||
|
||||
###### required parameters for GKE
|
||||
|
||||
| Name | Description |
|
||||
| ----------- | ------------------------------------------------------------------------ |
|
||||
| projectId | the ID of the GCP project containing your Kubernetes clusters |
|
||||
| region | the region of GCP containing your Kubernetes clusters |
|
||||
| clusterName | the name of your kubernetes cluster, within your `projectId` GCP project |
|
||||
| Name | Description |
|
||||
| ------------- | ------------------------------------------------------------------------ |
|
||||
| `projectId` | the ID of the GCP project containing your Kubernetes clusters |
|
||||
| `region` | the region of GCP containing your Kubernetes clusters |
|
||||
| `clusterName` | the name of your kubernetes cluster, within your `projectId` GCP project |
|
||||
|
||||
Note that the GKE cluster locator can automatically provide the values for the
|
||||
`dashboardApp` and `dashboardParameters` options if you set the
|
||||
|
||||
@@ -110,7 +110,7 @@ metadata:
|
||||
backstage.io/edit-url: https://github.com/my-org/catalog/edit/master/my-service.jsonnet
|
||||
```
|
||||
|
||||
These annotations allow customising links from the catalog pages. The view URL
|
||||
These annotations allow customizing links from the catalog pages. The view URL
|
||||
should point to the canonical metadata YAML that governs this entity. The edit
|
||||
URL should point to the source file for the metadata. In the example above,
|
||||
`my-org` generates its catalog data from Jsonnet files in a monorepo, so the
|
||||
@@ -159,11 +159,11 @@ metadata:
|
||||
github.com/project-slug: backstage/backstage
|
||||
```
|
||||
|
||||
The value of this annotation is the so-called slug that identifies a project on
|
||||
The value of this annotation is the so-called slug that identifies a repository on
|
||||
[GitHub](https://github.com) (either the public one, or a private GitHub
|
||||
Enterprise installation) that is related to this entity. It is on the format
|
||||
`<organization>/<project>`, and is the same as can be seen in the URL location
|
||||
bar of the browser when viewing that project.
|
||||
`<organization or owner>/<repository>`, and is the same as can be seen in the URL location
|
||||
bar of the browser when viewing that repository.
|
||||
|
||||
Specifying this annotation will enable GitHub related features in Backstage for
|
||||
that entity.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -81,8 +81,7 @@ import {
|
||||
scaffolderPlugin,
|
||||
createScaffolderFieldExtension,
|
||||
} from '@backstage/plugin-scaffolder';
|
||||
import { MyCustomExtension } from './MyCustomExtension';
|
||||
import { myCustomValidation } from './validation';
|
||||
import { MyCustomExtension, myCustomValidation } from './MyCustomExtension';
|
||||
|
||||
export const MyCustomFieldExtension = scaffolderPlugin.provide(
|
||||
createScaffolderFieldExtension({
|
||||
|
||||
@@ -111,7 +111,7 @@ in Backstage. While a default table experience, similar to the one provided by
|
||||
the Catalog plugin, is made available for ease-of-use, it's possible for you to
|
||||
provide a completely custom experience, tailored to the needs of your
|
||||
organization. For example, TechDocs comes with an alternative grid based layout
|
||||
(`<EntityListDocsGrid>`).
|
||||
(`<EntityListDocsGrid>`) and panel layout (`TechDocsCustomHome`).
|
||||
|
||||
This is done in your `app` package. By default, you might see something like
|
||||
this in your `App.tsx`:
|
||||
@@ -126,18 +126,120 @@ const AppRoutes = () => {
|
||||
};
|
||||
```
|
||||
|
||||
### Using TechDocsCustomHome
|
||||
|
||||
You can easily customize the TechDocs home page using TechDocs panel layout
|
||||
(`<TechDocsCustomHome />`).
|
||||
|
||||
Modify your `App.tsx` as follows:
|
||||
|
||||
```tsx
|
||||
import { TechDocsCustomHome } from '@backstage/plugin-techdocs';
|
||||
//...
|
||||
|
||||
const techDocsTabsConfig = [
|
||||
{
|
||||
label: "Recommended Documentation",
|
||||
panels: [
|
||||
{
|
||||
title: 'Golden Path',
|
||||
description: 'Documentation about standards to follow',
|
||||
panelType: 'DocsCardGrid',
|
||||
filterPredicate: entity => entity?.metadata?.tags?.includes('recommended') ?? false,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const AppRoutes = () => {
|
||||
<FlatRoutes>
|
||||
<Route path="/docs" element={<TechDocsCustomHome tabsConfig={techDocsTabsConfig} />}>
|
||||
</FlatRoutes>;
|
||||
};
|
||||
```
|
||||
|
||||
### Building a Custom home page
|
||||
|
||||
But you can replace `<DefaultTechDocsHome />` with any React component, which
|
||||
will be rendered in its place. Most likely, you would want to create and
|
||||
maintain such a component in a new directory at
|
||||
`packages/app/src/components/techdocs`, and import and use it in `App.tsx`:
|
||||
|
||||
For example, you can define the following Custom home page component:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
|
||||
import { Content } from '@backstage/core-components';
|
||||
import {
|
||||
CatalogFilterLayout,
|
||||
EntityOwnerPicker,
|
||||
EntityTagPicker,
|
||||
UserListPicker,
|
||||
EntityListProvider,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
TechDocsPageWrapper,
|
||||
TechDocsPicker,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
import {
|
||||
EntityListDocsGrid,
|
||||
DocsGroupConfig,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
|
||||
export type CustomTechDocsHomeProps = {
|
||||
groups?: Array<{
|
||||
title: React.ReactNode;
|
||||
filterPredicate: (entity: Entity) => boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const CustomTechDocsHome = ({ groups }: CustomTechDocsHomeProps) => {
|
||||
return (
|
||||
<TechDocsPageWrapper>
|
||||
<Content>
|
||||
<EntityListProvider>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<TechDocsPicker />
|
||||
<UserListPicker initialFilter="all" />
|
||||
<EntityOwnerPicker />
|
||||
<EntityTagPicker />
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<EntityListDocsGrid groups={groups} />
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</EntityListProvider>
|
||||
</Content>
|
||||
</TechDocsPageWrapper>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Then you can add the following to your `App.tsx`:
|
||||
|
||||
```tsx
|
||||
import { CustomTechDocsHome } from './components/techdocs/CustomTechDocsHome';
|
||||
// ...
|
||||
const AppRoutes = () => {
|
||||
<FlatRoutes>
|
||||
<Route path="/docs" element={<TechDocsIndexPage />}>
|
||||
<CustomTechDocsHome />
|
||||
<CustomTechDocsHome
|
||||
groups={[
|
||||
{
|
||||
title: 'Recommended Documentation',
|
||||
filterPredicate: entity =>
|
||||
entity?.metadata?.tags?.includes('recommended') ?? false,
|
||||
},
|
||||
{
|
||||
title: 'My Docs',
|
||||
filterPredicate: 'ownedByUser',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Route>
|
||||
</FlatRoutes>;
|
||||
};
|
||||
@@ -437,7 +539,7 @@ FROM python:3.8-alpine
|
||||
|
||||
RUN apk update && apk --no-cache add gcc musl-dev openjdk11-jdk curl graphviz ttf-dejavu fontconfig
|
||||
|
||||
RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==1.0.1
|
||||
RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==1.1.7
|
||||
|
||||
RUN pip install mkdocs-kroki-plugin
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Documentation on Customizing look and feel of the App
|
||||
|
||||
Backstage ships with a default theme with a light and dark mode variant. The
|
||||
themes are provided as a part of the
|
||||
[@backstage/theme](https://www.npmjs.com/package/@backstage/theme) package,
|
||||
[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package,
|
||||
which also includes utilities for customizing the default theme, or creating
|
||||
completely new themes.
|
||||
|
||||
@@ -14,7 +14,7 @@ completely new themes.
|
||||
|
||||
The easiest way to create a new theme is to use the `createTheme` function
|
||||
exported by the
|
||||
[@backstage/theme](https://www.npmjs.com/package/@backstage/theme) package. You
|
||||
[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You
|
||||
can use it to override some basic parameters of the default theme such as the
|
||||
color palette and font.
|
||||
|
||||
@@ -33,16 +33,16 @@ const myTheme = createTheme({
|
||||
|
||||
If you want more control over the theme, and for example customize font sizes
|
||||
and margins, you can use the lower-level `createThemeOverrides` function
|
||||
exported by [@backstage/theme](https://www.npmjs.com/package/@backstage/theme)
|
||||
exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme)
|
||||
in combination with
|
||||
[createTheme](https://material-ui.com/customization/theming/#createmuitheme-options-args-theme)
|
||||
from [@material-ui/core](https://www.npmjs.com/package/@material-ui/core). See
|
||||
[`createTheme`](https://material-ui.com/customization/theming/#createmuitheme-options-args-theme)
|
||||
from [`@material-ui/core`](https://www.npmjs.com/package/@material-ui/core). See
|
||||
the "Overriding Backstage and Material UI css rules" section below.
|
||||
|
||||
You can also create a theme from scratch that matches the `BackstageTheme` type
|
||||
exported by [@backstage/theme](https://www.npmjs.com/package/@backstage/theme).
|
||||
exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme).
|
||||
See the
|
||||
[material-ui docs on theming](https://material-ui.com/customization/theming/)
|
||||
[Material-UI docs on theming](https://material-ui.com/customization/theming/)
|
||||
for more information about how that can be done.
|
||||
|
||||
## Using your Custom Theme
|
||||
@@ -79,7 +79,7 @@ const app = createApp({
|
||||
Note that your list of custom themes overrides the default themes. If you still
|
||||
want to use the default themes, they are exported as `lightTheme` and
|
||||
`darkTheme` from
|
||||
[@backstage/theme](https://www.npmjs.com/package/@backstage/theme).
|
||||
[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme).
|
||||
|
||||
## Example of a custom theme
|
||||
|
||||
|
||||
@@ -284,27 +284,25 @@ otherwise something went terribly wrong.
|
||||
|
||||
## Create a new component using a software template
|
||||
|
||||
- Go to `create` and choose to create a website with the `React SSR Template`
|
||||
- Type in a name, let's use `tutorial`
|
||||
- Select the group `team-a` which will own this new website, and go to the next
|
||||
step
|
||||
|
||||
- Go to `create` and choose to create a website with the `Example Node.js Template`
|
||||
- Type in a name, let's use `tutorial` and click `Next Step`
|
||||
<p align='center'>
|
||||
<img src='../assets/getting-started/b-scaffold-1.png' alt='Software template deployment input screen asking for a name, the group owning this, and a description' />
|
||||
<img src='../assets/getting-started/b-scaffold-1.png' alt='Software template deployment input screen asking for a name' />
|
||||
</p>
|
||||
|
||||
- For the location, we're going to use the default
|
||||
- As owner, type your GitHub username
|
||||
- For the repository name, type `tutorial`. Go to the next step
|
||||
|
||||
- You should see the following screen:
|
||||
<p align='center'>
|
||||
<img src='../assets/getting-started/b-scaffold-2.png' alt='Software template deployment input screen asking for the GitHub username, and name of the new repo to create' />
|
||||
</p>
|
||||
|
||||
- For host, it should default to github.com
|
||||
- As owner, type your GitHub username
|
||||
- For the repository name, type `tutorial`. Go to the next step
|
||||
|
||||
- Review the details of this new service, and press `Create` if you want to
|
||||
deploy it like this.
|
||||
- You can follow along with the progress, and as soon as every step is
|
||||
finished, you can take a look at your new service
|
||||
- You can follow along with the progress, and as soon as every step is
|
||||
finished, you can take a look at your new service
|
||||
|
||||
Achievement unlocked. You've set up an installation of the core Backstage App,
|
||||
made it persistent, and configured it so you are now able to use software
|
||||
|
||||
@@ -51,14 +51,10 @@ create a subdirectory inside your current working directory.
|
||||
npx @backstage/create-app
|
||||
```
|
||||
|
||||
The wizard will ask you
|
||||
|
||||
- The name of the app, which will also be the name of the directory
|
||||
- The database type to use for the backend. For this guide, you'll be using the
|
||||
SQLite option.
|
||||
The wizard will ask you for the name of the app, which will also be the name of the directory
|
||||
|
||||
<p align='center'>
|
||||
<img src='../assets/getting-started/wizard.png' alt='Screenshot of the wizard asking for a name for the app, and a selection menu for the database.' />
|
||||
<img src='../assets/getting-started/wizard.png' alt='Screenshot of the wizard asking for a name for the app.' />
|
||||
</p>
|
||||
|
||||
### Run the Backstage app
|
||||
|
||||
@@ -32,6 +32,11 @@ catalog:
|
||||
bucketName: sample-bucket
|
||||
prefix: prefix/ # optional
|
||||
region: us-east-2 # optional, uses the default region otherwise
|
||||
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 }
|
||||
```
|
||||
|
||||
For simple setups, you can omit the provider ID at the config
|
||||
@@ -47,6 +52,11 @@ catalog:
|
||||
bucketName: sample-bucket
|
||||
prefix: prefix/ # optional
|
||||
region: us-east-2 # optional, uses the default region otherwise
|
||||
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 }
|
||||
```
|
||||
|
||||
As this provider is not one of the default providers, you will first need to install
|
||||
@@ -69,10 +79,13 @@ const builder = await CatalogBuilder.create(env);
|
||||
builder.addEntityProvider(
|
||||
AwsS3EntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
// optional: alternatively, use schedule
|
||||
scheduler: env.scheduler,
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
@@ -42,12 +42,17 @@ catalog:
|
||||
project: myproject
|
||||
repository: service-* # this will match all repos starting with service-*
|
||||
path: /catalog-info.yaml
|
||||
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 }
|
||||
anotherProviderId: # another identifier
|
||||
organization: myorg
|
||||
project: myproject
|
||||
repository: '*' # this will match all repos
|
||||
path: /src/*/catalog-info.yaml # this will search for files deep inside the /src folder
|
||||
yetAotherProviderId: # guess, what? Another one :)
|
||||
yetAnotherProviderId: # guess, what? Another one :)
|
||||
host: selfhostedazure.yourcompany.com
|
||||
organization: myorg
|
||||
project: myproject
|
||||
@@ -55,11 +60,20 @@ catalog:
|
||||
|
||||
The parameters available are:
|
||||
|
||||
- `host:` Leave empty for Cloud hosted, otherwise set to your self-hosted instance host.
|
||||
- `organization:` Your Organization slug (or Collection for on-premise users). Required.
|
||||
- `project:` Your project slug. Required.
|
||||
- `repository:` The repository name. Wildcards are supported as show on the examples above. If not set, all repositories will be searched.
|
||||
- `path:` Where to find catalog-info.yaml files. Defaults to /catalog-info.yaml.
|
||||
- **`host:`** _(optional)_ Leave empty for Cloud hosted, otherwise set to your self-hosted instance host.
|
||||
- **`organization:`** Your Organization slug (or Collection for on-premise users). Required.
|
||||
- **`project:`** Your project slug. Required.
|
||||
- **`repository:`** _(optional)_ The repository name. Wildcards are supported as show on the examples above. If not set, all repositories will be searched.
|
||||
- **`path:`** _(optional)_ Where to find catalog-info.yaml files. Defaults to /catalog-info.yaml.
|
||||
- **`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.
|
||||
|
||||
_Note:_ the path parameter follows the same rules as the search on Azure DevOps
|
||||
web interface. For more details visit the
|
||||
@@ -84,10 +98,13 @@ const builder = await CatalogBuilder.create(env);
|
||||
+builder.addEntityProvider(
|
||||
+ AzureDevOpsEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ // optional: alternatively, use scheduler with schedule defined in app-config.yaml
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: Duration.fromObject({ minutes: 30 }),
|
||||
+ timeout: Duration.fromObject({ minutes: 3 }),
|
||||
+ frequency: { minutes: 30 },
|
||||
+ timeout: { minutes: 3 },
|
||||
+ }),
|
||||
+ // optional: alternatively, use schedule
|
||||
+ scheduler: env.scheduler,
|
||||
+ }),
|
||||
+);
|
||||
```
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
---
|
||||
id: discovery
|
||||
title: Bitbucket Discovery
|
||||
sidebar_label: Discovery
|
||||
# prettier-ignore
|
||||
description: Automatically discovering catalog entities from repositories in Bitbucket
|
||||
---
|
||||
|
||||
The Bitbucket integration has a special discovery processor for discovering
|
||||
catalog entities located in Bitbucket. The processor will crawl your Bitbucket
|
||||
account and register entities matching the configured path. This can be useful
|
||||
as an alternative to static locations or manually adding things to the catalog.
|
||||
|
||||
## Installation
|
||||
|
||||
You will have to add the processor in the catalog initialization code of your
|
||||
backend. The provider is not installed by default, therefore you have to add a
|
||||
dependency to `@backstage/plugin-catalog-backend-module-bitbucket` to your backend
|
||||
package.
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-bitbucket
|
||||
```
|
||||
|
||||
And then add the processor to your catalog builder:
|
||||
|
||||
```diff
|
||||
// In packages/backend/src/plugins/catalog.ts
|
||||
+import { BitbucketDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-bitbucket';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
+ builder.addProcessor(
|
||||
+ BitbucketDiscoveryProcessor.fromConfig(env.config, { logger: env.logger })
|
||||
+ );
|
||||
```
|
||||
|
||||
## Self-hosted Bitbucket Server
|
||||
|
||||
To use the discovery processor with a self-hosted Bitbucket Server, you'll need
|
||||
a Bitbucket integration [set up](../bitbucketServer/locations.md) with a `BITBUCKET_TOKEN` and a
|
||||
`BITBUCKET_API_BASE_URL`. Then you can add a location target to the catalog
|
||||
configuration:
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
locations:
|
||||
- type: bitbucket-discovery
|
||||
target: https://bitbucket.mycompany.com/projects/my-project/repos/service-*/catalog-info.yaml
|
||||
```
|
||||
|
||||
Note the `bitbucket-discovery` type, as this is not a regular `url` processor.
|
||||
|
||||
The target is composed of four parts:
|
||||
|
||||
- The base instance URL, `https://bitbucket.mycompany.com` in this case
|
||||
- The project key to scan, which accepts \* wildcard tokens. This can simply be
|
||||
`*` to scan repositories from all projects. This example only scans for
|
||||
repositories in the `my-project` project.
|
||||
- The repository blob to scan, which accepts \* wildcard tokens. This can simply
|
||||
be `*` to scan all repositories in the project. This example only looks for
|
||||
repositories prefixed with `service-`.
|
||||
- The path within each repository to find the catalog YAML file. This will
|
||||
usually be `/catalog-info.yaml` or a similar variation for catalog files
|
||||
stored in the root directory of each repository. If omitted, the default value
|
||||
`catalog-info.yaml` will be used. E.g. given that `my-project`and `service-a`
|
||||
exists, `https://bitbucket.mycompany.com/projects/my-project/repos/service-*/`
|
||||
will result in:
|
||||
`https://bitbucket.mycompany.com/projects/my-project/repos/service-a/catalog-info.yaml`.
|
||||
|
||||
## Bitbucket Cloud
|
||||
|
||||
To use the discovery processor with Bitbucket Cloud, you'll need a Bitbucket
|
||||
integration [set up](../bitbucketCloud/locations.md) with a `username` and an `appPassword`. Then
|
||||
you can add a location target to the catalog configuration:
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
locations:
|
||||
- type: bitbucket-discovery
|
||||
target: https://bitbucket.org/workspaces/my-workspace
|
||||
```
|
||||
|
||||
Note the `bitbucket-discovery` type, as this is not a regular `url` processor.
|
||||
|
||||
The target is composed of the following parts:
|
||||
|
||||
- The base URL for Bitbucket, `https://bitbucket.org`
|
||||
- The workspace name to scan (following the `workspaces/` path part), which must
|
||||
match a workspace accessible with the username of your integration.
|
||||
- (Optional) The project key to scan (following the `projects/` path part),
|
||||
which accepts \* wildcard tokens. If omitted, repositories from all projects
|
||||
in the workspace are included.
|
||||
- (Optional) The repository blob to scan (following the `repos/` path part),
|
||||
which accepts \* wildcard tokens. If omitted, all repositories in the
|
||||
workspace are included.
|
||||
- (Optional) The `catalogPath` query argument to specify the location within
|
||||
each repository to find the catalog YAML file. This will usually be
|
||||
`/catalog-info.yaml` or a similar variation for catalog files stored in the
|
||||
root directory of each repository. If omitted, the default value
|
||||
`catalog-info.yaml` will be used.
|
||||
- (Optional) The `q` query argument to be passed through to Bitbucket for
|
||||
filtering results via the API. This is the most flexible option and will
|
||||
reduce the amount of API calls if you have a large workspace.
|
||||
[See here for the specification](https://developer.atlassian.com/bitbucket/api/2/reference/meta/filtering)
|
||||
for the query argument (will be passed as the `q` query parameter).
|
||||
- (Optional) The `search=true` query argument to activate the mode utilizing code search.
|
||||
- Is mutually exclusive to the `q` query argument.
|
||||
- Allows providing values at `catalogPath` for finding catalog files 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).
|
||||
- `catalogPath=/catalog-info.yaml`
|
||||
- `catalogPath=catalog-info.yaml` (anywhere in the repository)
|
||||
- `catalogPath=/path/catalog-info.yaml`
|
||||
- `catalogPath=path/catalog-info.yaml`
|
||||
- `catalogPath=/path/*/catalog-info.yaml`
|
||||
- `catalogPath=path/*/catalog-info.yaml`
|
||||
- Supports multiple catalog files per repository depending on the `catalogPath` value.
|
||||
- Registers `Location` entities for existing files only vs all matching repositories.
|
||||
|
||||
Examples:
|
||||
|
||||
- `https://bitbucket.org/workspaces/my-workspace/projects/my-project` will find
|
||||
all repositories in the `my-project` project in the `my-workspace` workspace.
|
||||
- `https://bitbucket.org/workspaces/my-workspace/repos/service-*` will find all
|
||||
repositories starting with `service-` in the `my-workspace` workspace.
|
||||
- `https://bitbucket.org/workspaces/my-workspace/projects/apis-*/repos/service-*`
|
||||
will find all repositories starting with `service-`, in all projects starting
|
||||
with `apis-` in the `my-workspace` workspace.
|
||||
- `https://bitbucket.org/workspaces/my-workspace?q=project.key ~ "my-project"`
|
||||
will find all repositories in a project containing `my-project` in its key.
|
||||
- `https://bitbucket.org/workspaces/my-workspace?catalogPath=my/nested/path/catalog.yaml`
|
||||
will find all repositories in the `my-workspace` workspace and use the catalog
|
||||
file at `my/nested/path/catalog.yaml`.
|
||||
- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=/catalog.yaml`
|
||||
will find all `catalog.yaml` files located in the root of repositories in the workspace `my-workspace`.
|
||||
- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=catalog.yaml`
|
||||
will find all `catalog.yaml` files located anywhere within repositories in the workspace `my-workspace`.
|
||||
- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=/my/nested/path/catalog.yaml`
|
||||
will find all `catalog.yaml` files located within the directory `/my/nested/path/` within
|
||||
repositories in the workspace `my-workspace`.
|
||||
- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=my/nested/path/catalog.yaml`
|
||||
will find all `catalog.yaml` files located within the directory `my/nested/path/` located anywhere within
|
||||
repositories in the workspace `my-workspace`.
|
||||
- `https://bitbucket.org/workspaces/my-workspace?search=true&catalogPath=/my/*/path/catalog.yaml`
|
||||
will find all `catalog.yaml` files located within a directory `path/` located within any (recursive) directory
|
||||
within the directory `my/` in the root of repositories in the workspace `my-workspace`
|
||||
(`/my/nested/path/catalog.yaml`, `/my/very/nested/path/catalog.yaml`, ...).
|
||||
- `https://bitbucket.org/workspaces/my-workspace/projects/apis-*/repos/service-*?search=true&catalogPath=catalog.yaml`
|
||||
will find all `catalog.yaml` files located anywhere within repositories starting with `service-`
|
||||
in projects starting with `api-` in the workspace `my-workspace`.
|
||||
|
||||
## Custom repository processing
|
||||
|
||||
The Bitbucket Discovery Processor will by default emit a location for each
|
||||
matching repository for further processing by other processors. However, it is
|
||||
possible to override this functionality and take full control of how each
|
||||
matching repository is processed.
|
||||
|
||||
`BitbucketDiscoveryProcessor.fromConfig` takes an optional parameter
|
||||
`options.parser` where you can set your own parser to be used for each matched
|
||||
repository.
|
||||
|
||||
```typescript
|
||||
const processor = BitbucketDiscoveryProcessor.fromConfig(env.config, {
|
||||
parser: async function* customRepositoryParser({ client, repository }) {
|
||||
// Custom logic for interpreting the matching repository.
|
||||
// See defaultRepositoryParser for an example
|
||||
},
|
||||
logger: env.logger,
|
||||
});
|
||||
```
|
||||
@@ -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,29 +68,37 @@ 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.
|
||||
|
||||
## Alternative
|
||||
|
||||
_Deprecated!_ Please raise issues for use cases not covered by the entity provider.
|
||||
|
||||
[You can use the `BitbucketDiscoveryProcessor`.](../bitbucket/discovery.md#bitbucket-cloud)
|
||||
|
||||
@@ -37,10 +37,13 @@ And then add the entity provider to your catalog builder:
|
||||
+ builder.addEntityProvider(
|
||||
+ BitbucketServerEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ // optional: alternatively, use scheduler with schedule defined in app-config.yaml
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { minutes: 30 },
|
||||
+ timeout: { minutes: 3 },
|
||||
+ }),
|
||||
+ // optional: alternatively, use schedule
|
||||
+ scheduler: env.scheduler,
|
||||
+ }),
|
||||
+ );
|
||||
|
||||
@@ -66,19 +69,33 @@ 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 }
|
||||
```
|
||||
|
||||
- **host**:
|
||||
- **`host`**:
|
||||
The host of the Bitbucket Server instance, **note**: the host needs to registered as an integration as well, see [location](locations.md).
|
||||
- **`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.
|
||||
- **filters** _(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.
|
||||
- **`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.
|
||||
|
||||
## Custom location processing
|
||||
|
||||
@@ -93,22 +110,13 @@ repository.
|
||||
```typescript
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
schedule: env.scheduler,
|
||||
parser: async function* customLocationParser(options: {
|
||||
location: LocationSpec,
|
||||
client: BitbucketServerClient,
|
||||
location: LocationSpec;
|
||||
client: BitbucketServerClient;
|
||||
}) {
|
||||
// Custom logic for interpreting the matching repository
|
||||
// See defaultBitbucketServerLocationParser for an example
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Alternative
|
||||
|
||||
_Deprecated!_ Please raise issues for use cases not covered by the entity provider.
|
||||
|
||||
[You can use the `BitbucketDiscoveryProcessor`.](../bitbucket/discovery.md#self-hosted-bitbucket-server)
|
||||
|
||||
@@ -32,10 +32,13 @@ const builder = await CatalogBuilder.create(env);
|
||||
builder.addEntityProvider(
|
||||
GerritEntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
// optional: alternatively, use schedule
|
||||
scheduler: env.scheduler,
|
||||
}),
|
||||
);
|
||||
```
|
||||
@@ -54,6 +57,11 @@ catalog:
|
||||
host: gerrit-your-company.com
|
||||
branch: master # Optional
|
||||
query: 'state=ACTIVE&prefix=webapps'
|
||||
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 }
|
||||
backend:
|
||||
host: gerrit-your-company.com
|
||||
branch: master # Optional
|
||||
@@ -62,8 +70,8 @@ catalog:
|
||||
|
||||
The provider configuration is composed of three parts:
|
||||
|
||||
- host, the host of the Gerrit integration to use.
|
||||
- branch, the branch where we will look for catalog entities (defaults to "master").
|
||||
- query, this string is directly used as the argument to the "List Project" API.
|
||||
Typically you will want to have some filter here to exclude projects that will
|
||||
- **`host`**: the host of the Gerrit integration to use.
|
||||
- **`branch`** _(optional)_: the branch where we will look for catalog entities (defaults to "master").
|
||||
- **`query`**: this string is directly used as the argument to the "List Project" API.
|
||||
Typically, you will want to have some filter here to exclude projects that will
|
||||
never contain any catalog files.
|
||||
|
||||
@@ -38,7 +38,7 @@ a structure with up to six elements:
|
||||
not set. The address used to clone a repo is the `cloneUrl` plus the repo name.
|
||||
- `gitilesBaseUrl` (optional): This is needed for creating a valid user-friendly URL
|
||||
that can be used for browsing the content of the provider. If not set a default
|
||||
value will be created in the same way as the "baseUrl" option. There is no
|
||||
value will be created in the same way as the `baseUrl` option. There is no
|
||||
requirement to have Gitiles for the Backstage Gerrit integration but without it
|
||||
some links in the Backstage UI will be broken.
|
||||
- `username` (optional): The Gerrit username to use in API requests. If
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
id: locations
|
||||
title: Gitea Locations
|
||||
sidebar_label: Locations
|
||||
description: Integrating source code stored in Gitea into the Backstage catalog
|
||||
---
|
||||
|
||||
The Gitea integration supports loading catalog entities from a hosted repository. Entities can be added to
|
||||
[static catalog configuration](../../features/software-catalog/configuration.md),
|
||||
registered with the
|
||||
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
|
||||
plugin.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use this integration, add configuration to your root `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
gitea:
|
||||
- host: gitea.example.com
|
||||
password: ${GITEA_TOKEN}
|
||||
- host: gitea.example.com
|
||||
username: ${GITEA_USERNAME}
|
||||
password: ${GITEA_PASSWORD}
|
||||
```
|
||||
|
||||
Directly under the `gitea` key is a list of provider configurations, where you
|
||||
can list the Gitea instances you want to be able to fetch
|
||||
data from. Each entry is a structure with up to four elements:
|
||||
|
||||
- `host`: The host of the gitea instance that you want to match on.
|
||||
- `baseUrl` (optional): Needed if the Gitea instance is not reachable at
|
||||
the base of the `host` option (e.g. `https://git.company.com/gitea`). This is the address that you would open in a browser.
|
||||
- `username` (optional): The gitea username to use in API requests.
|
||||
- `password` (optional): The password or api token to authenticate with.
|
||||
|
||||
You may supply only the `password` field, if authenticating via API access tokens (generated in Settings > Applications).
|
||||
@@ -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
|
||||
|
||||
@@ -30,19 +30,22 @@ And then add the entity provider to your catalog builder:
|
||||
|
||||
```diff
|
||||
// In packages/backend/src/plugins/catalog.ts
|
||||
+ import { GitHubEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
|
||||
+ import { GithubEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
+ builder.addEntityProvider(
|
||||
+ GitHubEntityProvider.fromConfig(env.config, {
|
||||
+ GithubEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ // optional: alternatively, use scheduler with schedule defined in app-config.yaml
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { minutes: 30 },
|
||||
+ timeout: { minutes: 3 },
|
||||
+ }),
|
||||
+ // optional: alternatively, use schedule
|
||||
+ scheduler: env.scheduler,
|
||||
+ }),
|
||||
+ );
|
||||
|
||||
@@ -55,7 +58,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:
|
||||
@@ -68,6 +71,11 @@ catalog:
|
||||
filters:
|
||||
branch: 'main' # string
|
||||
repository: '.*' # Regex
|
||||
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 }
|
||||
customProviderId:
|
||||
organization: 'new-org' # string
|
||||
catalogPath: '/custom/path/catalog-info.yaml' # string
|
||||
@@ -96,6 +104,13 @@ catalog:
|
||||
topic:
|
||||
include: ['backstage-include'] # optional array of strings
|
||||
exclude: ['experiments'] # optional array of strings
|
||||
validateLocationsExist:
|
||||
organization: 'backstage' # string
|
||||
catalogPath: '/catalog-info.yaml' # string
|
||||
filters:
|
||||
branch: 'main' # string
|
||||
repository: '.*' # Regex
|
||||
validateLocationsExist: true # optional boolean
|
||||
enterpriseProviderId:
|
||||
host: ghe.example.net
|
||||
organization: 'backstage' # string
|
||||
@@ -110,27 +125,43 @@ This provider supports multiple organizations via unique provider IDs.
|
||||
- **`catalogPath`** _(optional)_:
|
||||
Default: `/catalog-info.yaml`.
|
||||
Path where to look for `catalog-info.yaml` files.
|
||||
You can use wildcards - `*` or `**` - to search the path and/or the filename
|
||||
- **filters** _(optional)_:
|
||||
- **branch** _(optional)_:
|
||||
You can use wildcards - `*` or `**` - to search the path and/or the filename.
|
||||
Wildcards cannot be used if the `validateLocationsExist` option is set to `true`.
|
||||
- **`filters`** _(optional)_:
|
||||
- **`branch`** _(optional)_:
|
||||
String used to filter results based on the branch name.
|
||||
- **repository** _(optional)_:
|
||||
- **`repository`** _(optional)_:
|
||||
Regular expression used to filter results based on the repository name.
|
||||
- **topic** _(optional)_:
|
||||
- **`topic`** _(optional)_:
|
||||
Both of the filters below may be used at the same time but the exclusion filter has the highest priority.
|
||||
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.
|
||||
- **`include`** _(optional)_:
|
||||
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.
|
||||
- **`exclude`** _(optional)_:
|
||||
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**:
|
||||
- **`host`** _(optional)_:
|
||||
The hostname of your GitHub Enterprise instance. It must match a host defined in [integrations.github](locations.md).
|
||||
- **`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).
|
||||
- **`validateLocationsExist`** _(optional)_:
|
||||
Whether to validate locations that exist before emitting them.
|
||||
This option avoids generating locations for catalog info files that do not exist in the source repository.
|
||||
Defaults to `false`.
|
||||
Due to limitations in the GitHub API's ability to query for repository objects, this option cannot be used in
|
||||
conjunction with wildcards in the `catalogPath`.
|
||||
- **`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.
|
||||
|
||||
## GitHub API Rate Limits
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ catalog:
|
||||
group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole project will be scanned
|
||||
entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml`
|
||||
projectPattern: /[\s\S]*/ # Optional. Filters found projects based on provided patter. Defaults to `/[\s\S]*/`, what means to not filter anything
|
||||
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 }
|
||||
```
|
||||
|
||||
As this provider is not one of the default providers, you will first need to install
|
||||
@@ -47,10 +52,13 @@ const builder = await CatalogBuilder.create(env);
|
||||
builder.addEntityProvider(
|
||||
...GitlabDiscoveryEntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
// optional: alternatively, use schedule
|
||||
scheduler: env.scheduler,
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
@@ -16,17 +16,13 @@ integrations are used by many Backstage core features and other plugins.
|
||||
|
||||
Each key under `integrations` is a separate configuration for a single external
|
||||
provider. Providers each have different configuration; here's an example of
|
||||
configuration to use both GitHub and Bitbucket:
|
||||
configuration to use GitHub:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
github:
|
||||
- host: github.com
|
||||
token: ${GITHUB_TOKEN}
|
||||
bitbucket:
|
||||
- host: bitbucket.org
|
||||
username: ${BITBUCKET_USERNAME}
|
||||
appPassword: ${BITBUCKET_APP_PASSWORD}
|
||||
```
|
||||
|
||||
See documentation for each type of integration for full details on
|
||||
|
||||
@@ -11,15 +11,21 @@ For some use cases, you may want to define custom [rules](./concepts.md#resource
|
||||
Plugins should export a rule factory that provides type-safety that ensures compatibility with the plugin's backend. The catalog plugin exports `createCatalogPermissionRule` from `@backstage/plugin-catalog-backend/alpha` for this purpose. Note: the `/alpha` path segment is temporary until this API is marked as stable. For this example, we'll define the rule in `packages/backend/src/plugins/permission.ts`, but you can put it anywhere that's accessible by your `backend` package.
|
||||
|
||||
```typescript
|
||||
import type { Entity } from '@backstage/plugin-catalog-model';
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
|
||||
import { createConditionFactory } from '@backstage/plugin-permission-node';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const isInSystemRule = createCatalogPermissionRule({
|
||||
name: 'IS_IN_SYSTEM',
|
||||
description: 'Checks if an entity is part of the system provided',
|
||||
resourceType: 'catalog-entity',
|
||||
apply: (resource: Entity, systemRef: string) => {
|
||||
paramsSchema: z.object({
|
||||
systemRef: z
|
||||
.string()
|
||||
.describe('SystemRef to check the resource is part of'),
|
||||
}),
|
||||
apply: (resource: Entity, { systemRef }) => {
|
||||
if (!resource.relations) {
|
||||
return false;
|
||||
}
|
||||
@@ -28,9 +34,9 @@ export const isInSystemRule = createCatalogPermissionRule({
|
||||
.filter(relation => relation.type === 'partOf')
|
||||
.some(relation => relation.targetRef === systemRef);
|
||||
},
|
||||
toQuery: (systemRef: string) => ({
|
||||
toQuery: ({ systemRef }) => ({
|
||||
key: 'relations.partOf',
|
||||
value: systemRef,
|
||||
values: [systemRef],
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -85,14 +91,14 @@ class TestPermissionPolicy implements PermissionPolicy {
|
||||
if (isResourcePermission(request.permission, 'catalog-entity')) {
|
||||
return createCatalogConditionalDecision(
|
||||
request.permission,
|
||||
- catalogConditions.isEntityOwner(
|
||||
- user?.identity.ownershipEntityRefs ?? [],
|
||||
- ),
|
||||
- catalogConditions.isEntityOwner({
|
||||
- claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
- }),
|
||||
+ {
|
||||
+ anyOf: [
|
||||
+ catalogConditions.isEntityOwner(
|
||||
+ user?.identity.ownershipEntityRefs ?? []
|
||||
+ ),
|
||||
+ catalogConditions.isEntityOwner({
|
||||
+ claims: user?.identity.ownershipEntityRefs ?? []
|
||||
+ }),
|
||||
+ isInSystem('interviewing')
|
||||
+ ]
|
||||
+ }
|
||||
|
||||
@@ -53,7 +53,6 @@ $ yarn workspace backend add @backstage/plugin-permission-backend
|
||||
2. Add the following to a new file, `packages/backend/src/plugins/permission.ts`. This adds the permission-backend router, and configures it with a policy which allows everything.
|
||||
|
||||
```typescript
|
||||
import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
@@ -119,7 +118,6 @@ permission:
|
||||
2. Update the PermissionPolicy in `packages/backend/src/plugins/permission.ts` to disable a permission that’s easy for us to test. This policy rejects any attempt to delete a catalog entity:
|
||||
|
||||
```diff
|
||||
import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
|
||||
@@ -48,7 +48,9 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
...
|
||||
|
||||
- import { InputError } from '@backstage/errors';
|
||||
- import { IdentityApi } from '@backstage/plugin-auth-node';
|
||||
+ import { InputError, NotAllowedError } from '@backstage/errors';
|
||||
+ import { getBearerTokenFromAuthorizationHeader, IdentityApi } from '@backstage/plugin-auth-node';
|
||||
+ import { PermissionEvaluator, AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
@@ -56,7 +58,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
identity: IdentityClient;
|
||||
identity: IdentityApi;
|
||||
+ permissions: PermissionEvaluator;
|
||||
}
|
||||
|
||||
@@ -69,11 +71,13 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
...
|
||||
|
||||
router.post('/todos', async (req, res) => {
|
||||
const token = IdentityClient.getBearerToken(req.header('authorization'));
|
||||
let author: string | undefined = undefined;
|
||||
|
||||
const user = token ? await identity.authenticate(token) : undefined;
|
||||
const user = await identity.getIdentity({ request: req });
|
||||
author = user?.identity.userEntityRef;
|
||||
+ const token = getBearerTokenFromAuthorizationHeader(
|
||||
+ req.header('authorization'),
|
||||
+ );
|
||||
+ const decision = (
|
||||
+ await permissions.authorize([{ permission: todoListCreatePermission }], {
|
||||
+ token,
|
||||
@@ -128,10 +132,8 @@ In order to test the logic above, the integrators of your backstage instance nee
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
|
||||
- import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
+ import {
|
||||
+ BackstageIdentityResponse,
|
||||
+ IdentityClient
|
||||
+ } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
PermissionPolicy,
|
||||
@@ -170,3 +172,117 @@ Let's flip the result back to `ALLOW` before moving on.
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
At this point everything is working but if you run `yarn tsc` you'll get some errors, let's fix those up.
|
||||
|
||||
First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`:
|
||||
|
||||
```diff
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
+ import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { createRouter } from './router';
|
||||
|
||||
+ const mockedAuthorize: jest.MockedFunction<PermissionEvaluator['authorize']> =
|
||||
+ jest.fn();
|
||||
+ const mockedPermissionQuery: jest.MockedFunction<
|
||||
+ PermissionEvaluator['authorizeConditional']
|
||||
+ > = jest.fn();
|
||||
|
||||
+ const permissionEvaluator: PermissionEvaluator = {
|
||||
+ authorize: mockedAuthorize,
|
||||
+ authorizeConditional: mockedPermissionQuery,
|
||||
+ };
|
||||
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
logger: getVoidLogger(),
|
||||
identity: {} as DefaultIdentityClient,
|
||||
+ permissions: toPermissionEvaluator,
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('returns ok', async () => {
|
||||
const response = await request(app).get('/health');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Then we want to update the `plugins/todo-list-backend/src/service/standaloneServer.ts`, first we need to add the `@backstage/plugin-permission-node` package to `plugins/todo-list-backend/package.json` and then we can make the following edits:
|
||||
|
||||
```diff
|
||||
import {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
SingleHostDiscovery,
|
||||
+ ServerTokenManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'todo-list-backend' });
|
||||
logger.debug('Starting application server...');
|
||||
const config = await loadBackendConfig({ logger, argv: process.argv });
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
+ const tokenManager = ServerTokenManager.fromConfig(config, {
|
||||
+ logger,
|
||||
+ });
|
||||
+ const permissions = ServerPermissionClient.fromConfig(config, {
|
||||
+ discovery,
|
||||
+ tokenManager,
|
||||
+ });
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
+ permissions,
|
||||
});
|
||||
|
||||
let service = createServiceBuilder(module)
|
||||
.setPort(options.port)
|
||||
.addRouter('/todo-list', router);
|
||||
if (options.enableCors) {
|
||||
service = service.enableCors({ origin: 'http://localhost:3000' });
|
||||
}
|
||||
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
```
|
||||
|
||||
Now when you run `yarn tsc` you should have no more errors.
|
||||
|
||||
@@ -84,6 +84,7 @@ Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append th
|
||||
```typescript
|
||||
import { makeCreatePermissionRule } from '@backstage/plugin-permission-node';
|
||||
import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';
|
||||
import { z } from 'zod';
|
||||
import { Todo, TodoFilter } from './todos';
|
||||
|
||||
export const createTodoListPermissionRule = makeCreatePermissionRule<
|
||||
@@ -95,10 +96,13 @@ export const isOwner = createTodoListPermissionRule({
|
||||
name: 'IS_OWNER',
|
||||
description: 'Should allow only if the todo belongs to the user',
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
apply: (resource: Todo, userId: string) => {
|
||||
paramsSchema: z.object({
|
||||
userId: z.string().describe('User ID to match on the resource')
|
||||
})
|
||||
apply: (resource: Todo, { userId }) => {
|
||||
return resource.author === userId;
|
||||
},
|
||||
toQuery: (userId: string) => {
|
||||
toQuery: ({ userId }) => {
|
||||
return {
|
||||
property: 'author',
|
||||
values: [userId],
|
||||
@@ -223,7 +227,9 @@ Let's go back to the permission policy's handle function and try to authorize ou
|
||||
+ if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
+ return createTodoListConditionalDecision(
|
||||
+ request.permission,
|
||||
+ todoListConditions.isOwner(user?.identity.userEntityRef),
|
||||
+ todoListConditions.isOwner({
|
||||
+ userId: user?.identity.userEntityRef ?? '',
|
||||
+ }),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
|
||||
@@ -144,7 +144,9 @@ import {
|
||||
+ ) {
|
||||
return createTodoListConditionalDecision(
|
||||
request.permission,
|
||||
todoListConditions.isOwner(user?.identity.userEntityRef),
|
||||
todoListConditions.isOwner({
|
||||
userId: user?.identity.userEntityRef
|
||||
}),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -118,7 +118,7 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
|
||||
- <Grid item>
|
||||
- <AddTodo onAdd={handleAdd} />
|
||||
- </Grid>
|
||||
+ <RequirePermission permission={todoListCreatePermission}>
|
||||
+ <RequirePermission permission={todoListCreatePermission} errorPage={<></>}>
|
||||
+ <Grid item>
|
||||
+ <AddTodo onAdd={handleAdd} />
|
||||
+ </Grid>
|
||||
@@ -165,3 +165,25 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
|
||||
```
|
||||
|
||||
Now you should find that the component for adding a todo list item does not render at all. Success!
|
||||
|
||||
You can also use `RequirePermission` to prevent access to routes as well. Here's how that would look in your `packages/app/src/App.tsx`:
|
||||
|
||||
```diff
|
||||
+ import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
+ <Route path="/todo-list" element={
|
||||
// You might want to create a "read" permission for this, we are just using this one as an example
|
||||
+ <RequirePermission permission={todoListCreatePermission}>
|
||||
+ <TodoListPage />
|
||||
+ </RequirePermission>
|
||||
</FlatRoutes>
|
||||
```
|
||||
|
||||
Now if you try to navigate to `https://localhost:3000/todo-list` you'll get and error page if you do not have permission.
|
||||
|
||||
@@ -70,9 +70,9 @@ Let's change the policy to the following:
|
||||
- };
|
||||
+ return createCatalogConditionalDecision(
|
||||
+ request.permission,
|
||||
+ catalogConditions.isEntityOwner(
|
||||
+ user?.identity.ownershipEntityRefs ?? [],
|
||||
+ ),
|
||||
+ catalogConditions.isEntityOwner({
|
||||
+ claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
+ }),
|
||||
+ );
|
||||
}
|
||||
|
||||
@@ -119,9 +119,9 @@ class TestPermissionPolicy implements PermissionPolicy {
|
||||
+ if (isResourcePermission(request.permission, 'catalog-entity')) {
|
||||
return createCatalogConditionalDecision(
|
||||
request.permission,
|
||||
catalogConditions.isEntityOwner(
|
||||
user?.identity.ownershipEntityRefs ?? [],
|
||||
),
|
||||
catalogConditions.isEntityOwner({
|
||||
claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,4 +23,5 @@ iconUrl: # Used as the src attribute for your logo.
|
||||
# You can provide an external url or add your logo under static/img and provide a path
|
||||
# relative to static/ e.g. img/my-logo.png
|
||||
npmPackageName: # Your npm package name E.g. '@backstage/plugin-<etc>' quotes are required
|
||||
addedDate: # The date plugin added to marketplace E.g. '2022-10-01' quotes are required
|
||||
```
|
||||
|
||||
@@ -52,12 +52,13 @@ learn how to contribute the integration yourself!
|
||||
The following table summarizes events that, depending on the plugins you have
|
||||
installed, may be captured.
|
||||
|
||||
| Action | Subject | Other Notes |
|
||||
| ---------- | --------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `navigate` | The URL of the page that was navigated to | |
|
||||
| `click` | The text of the link that was clicked on | The `to` attribute represents the URL clicked to |
|
||||
| `search` | The search term entered in any search bar component | The `searchTypes` attribute holds `types` constraining the search |
|
||||
| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided |
|
||||
| Action | Subject | Other Notes |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
|
||||
| `navigate` | The URL of the page that was navigated to | |
|
||||
| `click` | The text of the link that was clicked on | The `to` attribute represents the URL clicked to |
|
||||
| `create` | The `name` of the software being created; if no `name` property is requested by the given Software Template, then the string `new {templateName}` is used instead. | The context holds an `entityRef`, set to the template's ref (e.g. `template:default/template-name`) |
|
||||
| `search` | The search term entered in any search bar component | The context holds `searchTypes`, representing `types` constraining the search |
|
||||
| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided |
|
||||
|
||||
If there is an event you'd like to see captured, please [open an
|
||||
issue][add-event] describing the event you want to see and the questions it
|
||||
@@ -301,11 +302,11 @@ it's important to keep each of these levels of detail disaggregated.
|
||||
automatically as part of the `extension` in which the `filter` event was
|
||||
captured).
|
||||
|
||||
- On the flip side, when adding `attributes` to an event, look at existing
|
||||
events and see if the data you are capturing matches the intention, type, or
|
||||
even the content of _their_ `attributes`. For instance, it may be common for
|
||||
events that involve the Catalog to add details like entity `name`, `kind`,
|
||||
and/or `namespace` as `attributes`. Using the same keys in your event will
|
||||
- On the flip side, when adding `attributes` to or `context` around an event,
|
||||
look at existing events and see if the data you are capturing matches the
|
||||
intention, type, or even the content of _their_ `attributes` or `context`.
|
||||
For instance, it's common for events that involve the Catalog to include an
|
||||
`entityRef` contextual key. Using the same keys and values in your event will
|
||||
ensure that events instrumented across plugins can easily be aggregated.
|
||||
|
||||
### Unit Testing Event Capture
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -196,14 +196,14 @@ const App = () => (
|
||||
There are a couple of naming patterns to adhere to as you build plugins, which
|
||||
helps clarify the intent and usage of the exports.
|
||||
|
||||
| Description | Pattern | Examples |
|
||||
| --------------------- | --------------- | ---------------------------------------------- |
|
||||
| Top-level Pages | \*Page | CatalogIndexPage, SettingsPage, LighthousePage |
|
||||
| Entity Tab Content | Entity\*Content | EntityJenkinsContent, EntityKubernetesContent |
|
||||
| Entity Overview Card | Entity\*Card | EntitySentryCard, EntityPagerDutyCard |
|
||||
| Entity Conditional | is\*Available | isPagerDutyAvailable, isJenkinsAvailable |
|
||||
| Plugin Instance | \*Plugin | jenkinsPlugin, catalogPlugin |
|
||||
| Utility API Reference | \*ApiRef | configApiRef, catalogApiRef |
|
||||
| Description | Pattern | Examples |
|
||||
| --------------------- | ----------------- | ---------------------------------------------------- |
|
||||
| Top-level Pages | `\*Page` | `CatalogIndexPage`, `SettingsPage`, `LighthousePage` |
|
||||
| Entity Tab Content | `Entity\*Content` | `EntityJenkinsContent`, `EntityKubernetesContent` |
|
||||
| Entity Overview Card | `Entity\*Card` | `EntitySentryCard`, `EntityPagerDutyCard` |
|
||||
| Entity Conditional | `is\*Available` | `isPagerDutyAvailable`, `isJenkinsAvailable` |
|
||||
| Plugin Instance | `\*Plugin` | `jenkinsPlugin`, `catalogPlugin` |
|
||||
| Utility API Reference | `\*ApiRef` | `configApiRef`, `catalogApiRef` |
|
||||
|
||||
### Routing System
|
||||
|
||||
@@ -515,10 +515,10 @@ deprecated while making the new additions, to then be removed at a later point.
|
||||
Many export naming patterns have been changed to avoid import aliases and to
|
||||
clarify intent. Refer to the following table to formulate the new name:
|
||||
|
||||
| Description | Existing Pattern | New Pattern | Examples |
|
||||
| -------------------- | -------------------------- | --------------- | ---------------------------------------------- |
|
||||
| Top-level Pages | Router | \*Page | CatalogIndexPage, SettingsPage, LighthousePage |
|
||||
| Entity Tab Content | Router | Entity\*Content | EntityJenkinsContent, EntityKubernetesContent |
|
||||
| Entity Overview Card | \*Card | Entity\*Card | EntitySentryCard, EntityPagerDutyCard |
|
||||
| Entity Conditional | isPluginApplicableToEntity | is\*Available | isPagerDutyAvailable, isJenkinsAvailable |
|
||||
| Plugin Instance | plugin | \*Plugin | jenkinsPlugin, catalogPlugin |
|
||||
| Description | Existing Pattern | New Pattern | Examples |
|
||||
| -------------------- | ---------------------------- | ----------------- | ---------------------------------------------------- |
|
||||
| Top-level Pages | `Router` | `\*Page` | `CatalogIndexPage`, `SettingsPage`, `LighthousePage` |
|
||||
| Entity Tab Content | `Router` | `Entity\*Content` | `EntityJenkinsContent`, `EntityKubernetesContent` |
|
||||
| Entity Overview Card | `\*Card` | `Entity\*Card` | `EntitySentryCard`, `EntityPagerDutyCard` |
|
||||
| Entity Conditional | `isPluginApplicableToEntity` | `is\*Available` | `isPagerDutyAvailable`, `isJenkinsAvailable` |
|
||||
| Plugin Instance | `plugin` | `\*Plugin` | `jenkinsPlugin`, `catalogPlugin` |
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
---
|
||||
id: new-backend-system
|
||||
title: New Backend System
|
||||
description: Details of the upcoming backend system
|
||||
---
|
||||
|
||||
> **DISCLAIMER: The new backend system is under active development and is not considered stable**
|
||||
|
||||
## Status
|
||||
|
||||
The new backend system is under active development, and only a small number of plugins and services have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet.
|
||||
|
||||
You can find an example backend setup at https://github.com/backstage/backstage/tree/master/packages/backend-next.
|
||||
|
||||
## Overview
|
||||
|
||||
The new Backstage backend system is being built to help make it simpler to install backend plugins and keep projects up to date. It also changes the foundation to one that makes it a lot easier to evolve plugins and the system itself. You can read more about the reasoning in the [original RFC](https://github.com/backstage/backstage/issues/11611).
|
||||
|
||||
One of the goals of the new system was to reduce the code needed for setting up a Backstage backend and installing plugins. This is an example of how you create, add features, and start up your backend in the new system:
|
||||
|
||||
```ts
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { catalogPlugin } from '@backstage/plugin-catalog-backend';
|
||||
|
||||
// Create your backend instance
|
||||
const backend = createBackend();
|
||||
|
||||
// Install all desired features
|
||||
backend.add(catalogPlugin());
|
||||
|
||||
// Start up the backend
|
||||
await backend.start();
|
||||
```
|
||||
|
||||
One notable change that helped achieve this much slimmer backend setup is the introduction of dependency injection, with a system that is very similar to the one in the Backstage frontend.
|
||||
|
||||
## Building Blocks
|
||||
|
||||
This section introduces the high-level building blocks upon which this new system is built. These are all concepts that exist in our current system in one way or another, but the have all been lifted up to be first class concerns in the new system.
|
||||
|
||||
### Backend
|
||||
|
||||
This is the backend instance itself, which you can think of as the unit of deployment. It does not have any functionality in itself, but is simply responsible for wiring things together.
|
||||
|
||||
It is up to you to decide how many different backends you want to deploy. You can have all features in a single one, or split things out into multiple smaller deployments. All depending on your need to scale and isolate individual features.
|
||||
|
||||
### Plugins
|
||||
|
||||
Plugins provide the actual features, just like in our existing system. They operate completely independently of each other. If plugins what to communicate with each other, they must do so over the wire. There can be no direct communication between plugins through code. Because of this constraints, each plugins can be considered to be its own microservice.
|
||||
|
||||
### Services
|
||||
|
||||
Services provide utilities to help make it simpler to implement plugins, so that each plugin doesn't need to implement everything from scratch. There are both many built-in services, like the ones for logging, database access, and reading configuration, but you can also import third-party services, or create your own.
|
||||
|
||||
Services are also a customization point for individual backend installations. You can both override services with your own implementations, as well as make smaller customizations to existing services.
|
||||
|
||||
### Extension Points
|
||||
|
||||
Many plugins have ways in which you can extend them, for example entity providers for the Catalog, or custom actions for the Scaffolder. These extension patterns are now encoded into Extension Points.
|
||||
|
||||
Extension Points look a little bit like services, since you depended on them just like you would a service. A key difference is that extension points are registered and provided by plugins themselves, based on what customizations each individual plugin wants to expose.
|
||||
|
||||
Extension Points are also exported separately from the plugin instance itself, and a single plugin can also expose multiple different extension points at once. This makes it easier to evolve and deprecated individual Extension Points over time, rather than dealing with a single large API surface.
|
||||
|
||||
### Modules
|
||||
|
||||
Modules use the plugin Extension Points to add new features for plugins. They might for example add an individual Catalog Entity Provider, or one or more Scaffolder Actions. Modules are basically plugins for plugins.
|
||||
|
||||
Each module may only extend a single plugin, and the module must be deployed together with that plugin in the same backend instance. Modules may however only communicate with their plugin through its registered extension points.
|
||||
|
||||
Just like plugins, modules also have access to services and can depend on their own service implementations. They will however share services with the plugin that they extend, there are no module-specific service implementations.
|
||||
|
||||
## Creating Plugins
|
||||
|
||||
Plugins are created using the `createBackendPlugin` function. All plugins must have an ID and a register method. Plugins may also accept an options object, which can be either optional or required. The options are passed to the second parameter of the register method, and the options type is inferred and forwarded to the returned plugin factory function.
|
||||
|
||||
```ts
|
||||
import {
|
||||
configServiceRef,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
// export type ExamplePluginOptions = { exampleOption: boolean };
|
||||
export const examplePlugin = createBackendPlugin({
|
||||
// unique id for the plugin
|
||||
id: 'example',
|
||||
// It's possible to provide options to the plugin
|
||||
// register(env, options: ExamplePluginOptions) {
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: loggerServiceRef,
|
||||
},
|
||||
// logger is provided by the backend based on the dependency on loggerServiceRef above.
|
||||
async init({ logger }) {
|
||||
logger.info('Hello from example plugin');
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The plugin can then be installed in the backend using the returned plugin factory function:
|
||||
|
||||
```ts
|
||||
backend.add(examplePlugin());
|
||||
```
|
||||
|
||||
If we wanted our plugin to accept options as well, we'd accept the options as the second parameter of the register method:
|
||||
|
||||
```ts
|
||||
export const examplePlugin = createBackendPlugin({
|
||||
id: 'example',
|
||||
register(env, options?: { silent?: boolean }) {
|
||||
env.registerInit({
|
||||
deps: { logger: loggerServiceRef },
|
||||
async init({ logger }) {
|
||||
if (!options?.silent) {
|
||||
logger.info('Hello from example plugin');
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Passing the option to the plugin during installation looks like this:
|
||||
|
||||
```ts
|
||||
backend.add(examplePlugin({ silent: true }));
|
||||
```
|
||||
|
||||
## Creating Modules
|
||||
|
||||
Some facts about modules
|
||||
|
||||
- A Module is able to extend a plugin with additional functionality using the `ExtensionPoint`s registered by the plugin.
|
||||
- A module can only extend one plugin but can interact with multiple `ExtensionPoint`s registered by that plugin.
|
||||
- A module is always initialized before the plugin it extends.
|
||||
|
||||
A module depends on the `ExtensionPoint`s exported by the target plugin's library package, for example `@backstage/plugin-catalog-node`, and does not directly declare a dependency on the plugin package itself.
|
||||
|
||||
Here's an example on how to create a module that adds a new processor using the `catalogProcessingExtensionPoint`:
|
||||
|
||||
```ts
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
|
||||
import { MyCustomProcessor } from './processor';
|
||||
|
||||
export const exampleCustomProcessorCatalogModule = createBackendModule({
|
||||
moduleId: 'exampleCustomProcessor',
|
||||
pluginId: 'catalog',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
},
|
||||
async init({ catalog }) {
|
||||
catalog.addProcessor(new MyCustomProcessor());
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Extension Points
|
||||
|
||||
Modules depend on extension points just as a regular dependency by specifying it in the `deps` section.
|
||||
|
||||
#### Defining an Extension Point
|
||||
|
||||
```ts
|
||||
import { createExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
|
||||
export interface ScaffolderActionsExtensionPoint {
|
||||
addAction(action: ScaffolderAction): void;
|
||||
}
|
||||
|
||||
export const scaffolderActionsExtensionPoint =
|
||||
createExtensionPoint<ScaffolderActionsExtensionPoint>({
|
||||
id: 'scaffolder.actions',
|
||||
});
|
||||
```
|
||||
|
||||
#### Registering an Extension Point
|
||||
|
||||
Extension points are registered by a plugin and extended by modules.
|
||||
|
||||
## Backend Services
|
||||
|
||||
The default backend provides several _services_ out of the box which includes access to configuration, logging, databases and more.
|
||||
Service dependencies are declared using their `ServiceRef`s in the `deps` section of the plugin or module, and the implementations are then forwarded to the `init` method of the plugin or module.
|
||||
|
||||
### Service References
|
||||
|
||||
A `ServiceRef` is a named reference to an interface which are later used to resolve the concrete service implementation. Conceptually this is very similar to `ApiRef`s in the frontend.
|
||||
Services is what provides common utilities that previously resided in the `PluginEnvironment` such as Config, Logging and Database.
|
||||
|
||||
On startup the backend will make sure that the services are initialized before being passed to the plugin/module that depend on them.
|
||||
ServiceRefs contain a scope which is used to determine if the serviceFactory creating the service will create a new instance scoped per plugin/module or if it will be shared. `plugin` scoped services will be created once per plugin/module and `root` scoped services will be created once per backend instance.
|
||||
|
||||
#### Defining a Service
|
||||
|
||||
```ts
|
||||
import {
|
||||
createServiceFactory,
|
||||
pluginMetadataServiceRef,
|
||||
loggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ExampleImpl } from './ExampleImpl';
|
||||
|
||||
export interface ExampleApi {
|
||||
doSomething(): Promise<void>;
|
||||
}
|
||||
|
||||
export const exampleServiceRef = createServiceRef<ExampleApi>({
|
||||
id: 'example',
|
||||
scope: 'plugin', // can be 'root' or 'plugin'
|
||||
|
||||
// The defaultFactory is optional to implement but it will be used if no other factory is provided to the backend.
|
||||
// This is allows for the backend to provide a default implementation of the service without having to wire it beforehand.
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {
|
||||
logger: loggerServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
// Logger is available directly in the factory as it's a root scoped service and will be created once per backend instance.
|
||||
async factory({ logger }) {
|
||||
// plugin is available as it's a plugin scoped service and will be created once per plugin.
|
||||
return async ({ plugin }) => {
|
||||
// This block will be executed once for every plugin that depends on this service
|
||||
logger.info('Initializing example service plugin instance');
|
||||
return new ExampleImpl({ logger, plugin });
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Overriding Services
|
||||
|
||||
In this example we replace the default root logger service implementation with a custom one that streams logs to GCP. The `rootLoggerServiceRef` has a `'root'` scope, meaning there are no plugin-specific instances of this service.
|
||||
|
||||
```ts
|
||||
import {
|
||||
createServiceFactory,
|
||||
rootLoggerServiceRef,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
// This custom implementation would typically live separately from
|
||||
// the backend setup code, either nearby such as in
|
||||
// packages/backend/src/services/logger/GoogleCloudLogger.ts
|
||||
// Or you can let it live in its own library package.
|
||||
class GoogleCloudLogger implements LoggerService {
|
||||
static factory = createServiceFactory({
|
||||
service: rootLoggerServiceRef,
|
||||
deps: {},
|
||||
async factory() {
|
||||
return new GoogleCloudLogger();
|
||||
},
|
||||
});
|
||||
// custom implementation here ...
|
||||
}
|
||||
|
||||
// packages/backend/src/index.ts
|
||||
const backend = createBackend({
|
||||
services: [
|
||||
// supplies additional or replacement services to the backend
|
||||
GoogleCloudLogger.factory(),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Utilities for testing backend plugins and modules are available in `@backstage/backend-test-utils`.
|
||||
|
||||
```ts
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('Example', () => {
|
||||
it('should do something', async () => {
|
||||
await startTestBackend({
|
||||
// mock services can be provided to the backend
|
||||
services: [someServiceFactory],
|
||||
// plugins and modules for testing
|
||||
features: [testModule()],
|
||||
});
|
||||
|
||||
// assertions
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Package structure
|
||||
|
||||
A detailed explanation of the package architecture can be found in the [Backstage Architecture Overview](../overview/architecture-overview.md#package-architecture). The most important packages to consider for this system are `backend`, `plugin-<pluginId>-backend`, `plugin-<pluginId>-node`, and `plugin-<pluginId>-backend-module-<moduleId>`.
|
||||
|
||||
- `plugin-<pluginId>-backend` houses the implementation of the plugins themselves.
|
||||
- `plugin-<pluginId>-node` houses the extension points and any other utilities that modules or other plugins might need.
|
||||
- `plugin-<pluginId>-backend-module-<moduleId>` houses the modules that extend the plugins via the extension points.
|
||||
- `backend` is the backend itself that wires everything together to something that you can deploy.
|
||||
@@ -83,7 +83,7 @@ export const ExamplePage = examplePlugin.provide(
|
||||
|
||||
This is where the plugin is created and where it creates and exports extensions
|
||||
that can be imported and used the app. See reference docs for
|
||||
[createPlugin](../reference/core-plugin-api.createplugin.md) or introduction to
|
||||
[`createPlugin`](../reference/core-plugin-api.createplugin.md) or introduction to
|
||||
the new [Composability System](./composability.md).
|
||||
|
||||
## Components
|
||||
|
||||
@@ -174,14 +174,14 @@ which can be used to request the provider's API.
|
||||
`read` then makes an authenticated request to the provider API and returns the
|
||||
file's content.
|
||||
|
||||
#### readUrl
|
||||
#### `readUrl`
|
||||
|
||||
`readUrl` is a new interface that allows complex response objects and is
|
||||
intended to replace the `read` method. This new method is currently optional to
|
||||
implement which allows for a soft migration to `readUrl` instead of `read` in
|
||||
the future.
|
||||
|
||||
#### readTree
|
||||
#### `readTree`
|
||||
|
||||
`readTree` method also expects user-friendly URLs similar to `read` but the URL
|
||||
should point to a tree (could be the root of a repository or even a
|
||||
@@ -241,8 +241,8 @@ without an `etag`, the response contains an ETag of the resource (should ideally
|
||||
forward the ETag returned by the provider). If the method is called with an
|
||||
`etag`, it first compares the ETag and returns a `NotModifiedError` in case the
|
||||
resource has not been modified. This approach is very similar to the actual
|
||||
[ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and
|
||||
[If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
|
||||
[`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and
|
||||
[`If-None-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
|
||||
HTTP headers.
|
||||
|
||||
### 6. Debugging
|
||||
|
||||
@@ -38,7 +38,7 @@ Server-to-server authentication tokens issued from a TokenManager (specifically,
|
||||
|
||||
### Kubernetes
|
||||
|
||||
Added support for `oidc` as authProvider for kubernetes authentication and added an optional `oidcTokenProvider` config value. This will allow users to authenticate to kubernetes clusters using ID tokens obtained from the configured auth provider in their Backstage instance. Contributed by @dbravovmw(https://github.com/dbravovmw). #11328(https://github.com/backstage/backstage/pull/11328)
|
||||
Added support for `oidc` as an auth provider for kubernetes authentication and added an optional `oidcTokenProvider` config value. This will allow users to authenticate to kubernetes clusters using ID tokens obtained from the configured auth provider in their Backstage instance. Contributed by @dbravovmw(https://github.com/dbravovmw). #11328(https://github.com/backstage/backstage/pull/11328)
|
||||
|
||||
### Misc
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
id: v1.7.0
|
||||
title: v1.7.0
|
||||
description: Backstage Release v1.7.0
|
||||
---
|
||||
|
||||
These are the release notes for the v1.7.0 release of
|
||||
[Backstage](https://backstage.io/).
|
||||
|
||||
A huge thanks to the whole team of maintainers and contributors as well as the
|
||||
amazing Backstage Community for the hard work in getting this release developed
|
||||
and done.
|
||||
|
||||
## Highlights
|
||||
|
||||
### GitHub Catalog Import now Powered by the Backend
|
||||
|
||||
The analysis performed during catalog imports (i.e. when supplying the URL of a
|
||||
repository rather than an individual YAML file in the Create flow) is now
|
||||
powered by the backend rather than frontend code. This means that the catalog
|
||||
backend needs to be supplied with a location analyzer for this use case to
|
||||
continue to function.
|
||||
|
||||
If you want to make use of this feature, check out the installation instructions
|
||||
in [the
|
||||
changelog](https://github.com/backstage/backstage/blob/master/plugins/catalog-import/CHANGELOG.md#090).
|
||||
|
||||
Contributed by [@kissmikijr](https://github.com/kissmikijr) in
|
||||
[#13800](https://github.com/backstage/backstage/pull/13800)
|
||||
|
||||
### Permission Rule Changes
|
||||
|
||||
When defining permission rules, it's now necessary to provide a [Zod
|
||||
Schema](https://github.com/colinhacks/zod) that specifies the parameters the
|
||||
rule expects. This has been added to help better describe the parameters in the
|
||||
response of the metadata endpoint and to validate the parameters before a rule
|
||||
is executed. The signatures of the rule methods (`apply` and `toQuery`) have
|
||||
changed slightly as well.
|
||||
|
||||
You can read more about this in [the permissions
|
||||
documentation](https://backstage.io/docs/permissions/overview) and [the
|
||||
changelog](https://github.com/backstage/backstage/blob/master/plugins/permission-node/CHANGELOG.md#070).
|
||||
|
||||
### Migration: `jest` v29
|
||||
|
||||
Both `jest`, `jest-runtime`, and `jest-environment-jsdom` as used by the
|
||||
Backstage CLI were bumped to version 29. This is up from version 27, so check
|
||||
out both the [v28](https://jestjs.io/docs/28.x/upgrading-to-jest28) and
|
||||
[v29](https://jestjs.io/docs/upgrading-to-jest29) (later
|
||||
[here](https://jestjs.io/docs/29.x/upgrading-to-jest29)) migration guides, since
|
||||
your existing tests may be affected.
|
||||
|
||||
Particular changes that were encountered in the main Backstage repository are:
|
||||
|
||||
- The updated snapshot format.
|
||||
- `jest.useFakeTimers('legacy')` is now `jest.useFakeTimers({ legacyFakeTimers: true })`.
|
||||
- Error objects collected by `withLogCollector` from `@backstage/test-utils` are
|
||||
now objects with a `detail` property rather than a string.
|
||||
|
||||
### Migration: `react-router` v6
|
||||
|
||||
Newly created Backstage repositories now use the stable version 6 of
|
||||
`react-router`, just like the main repository does.
|
||||
|
||||
Migrating to the stable version of `react-router` is optional for the time
|
||||
being; Backstage has support for both versions. But if you want to do the same
|
||||
for your existing repository, please follow [this
|
||||
guide](https://backstage.io/docs/tutorials/react-router-stable-migration).
|
||||
Support for the beta version will be removed in a later release.
|
||||
|
||||
### Support for `__mocks__` and `__testUtils__` directories
|
||||
|
||||
The Backstage CLI now has built-in support for `__mocks__` and `__testUtils__`
|
||||
directories in your code. These can be used for mocks and shared utilities in
|
||||
tests.
|
||||
|
||||
### New Arguments for the Router of `@backstage/plugin-bazaar-backend`
|
||||
|
||||
The bazaar-backend `createRouter` function now requires that the `identityApi`
|
||||
is passed to the router.
|
||||
|
||||
### Deprecated plugin: `@backstage/plugin-catalog-backend-module-bitbucket`
|
||||
|
||||
This has been deprecated and split into
|
||||
`@backstage/plugin-catalog-backend-module-bitbucket-cloud` and
|
||||
`@backstage/plugin-catalog-backend-module-bitbucket-server`, for BitBucket Cloud
|
||||
and BitBucket Server respectively. Please update your dependencies accordingly,
|
||||
depending on which product you use.
|
||||
|
||||
The original package will be removed in a future release.
|
||||
|
||||
Contributed by [@pjungermann](https://github.com/pjungermann) in
|
||||
[#14070](https://github.com/backstage/backstage/pull/14070)
|
||||
|
||||
## Security Fixes
|
||||
|
||||
This release does not contain any security fixes.
|
||||
|
||||
## Upgrade path
|
||||
|
||||
We recommend that you keep your Backstage project up to date with this latest
|
||||
release. For more guidance on how to upgrade, check out the documentation for
|
||||
[keeping Backstage
|
||||
updated](https://backstage.io/docs/getting-started/keeping-backstage-updated).
|
||||
|
||||
## Links and References
|
||||
|
||||
Below you can find a list of links and references to help you learn about and
|
||||
start using this new release.
|
||||
|
||||
- [Backstage official website](https://backstage.io/),
|
||||
[documentation](https://backstage.io/docs/), and [getting started
|
||||
guide](https://backstage.io/docs/getting-started/)
|
||||
- [GitHub repository](https://github.com/backstage/backstage)
|
||||
- Backstage's [versioning and support
|
||||
policy](https://backstage.io/docs/overview/versioning-policy)
|
||||
- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support
|
||||
- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.7.0-changelog.md)
|
||||
- Backstage [Demos](https://backstage.io/demos),
|
||||
[Blog](https://backstage.io/blog),
|
||||
[Roadmap](https://backstage.io/docs/overview/roadmap) and
|
||||
[Plugins](https://backstage.io/plugins)
|
||||
|
||||
Sign up for our [newsletter](https://mailchi.mp/spotify/backstage-community) if
|
||||
you want to be informed about what is happening in the world of Backstage.
|
||||
@@ -178,7 +178,7 @@ When migrating over to React Router v6 stable, you might also see browser consol
|
||||
|
||||
```diff
|
||||
- <Navigate key="/" to="catalog" />
|
||||
+ <Route path="/" element={<Navigate to="/catalog" />} />
|
||||
+ <Route path="/" element={<Navigate to="catalog" />} />
|
||||
```
|
||||
|
||||
### `NavLink`
|
||||
|
||||
@@ -72,6 +72,12 @@ backend:
|
||||
+ port: ${POSTGRES_PORT}
|
||||
+ user: ${POSTGRES_USER}
|
||||
+ password: ${POSTGRES_PASSWORD}
|
||||
+ # https://node-postgres.com/features/ssl
|
||||
+ # you can set the sslmode configuration option via the `PGSSLMODE` environment variable
|
||||
+ # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require)
|
||||
+ # ssl:
|
||||
+ # ca: # if you have a CA file and want to verify it you can uncomment this section
|
||||
+ # $file: <file-path>/ca/server.crt
|
||||
+ # Refer to Tarn docs for default values on PostgreSQL pool configuration - https://github.com/Vincit/tarn.js
|
||||
+ knexConfig:
|
||||
+ pool:
|
||||
@@ -79,12 +85,6 @@ backend:
|
||||
+ max: 12
|
||||
+ acquireTimeoutMillis: 60000
|
||||
+ idleTimeoutMillis: 60000
|
||||
+ # https://node-postgres.com/features/ssl
|
||||
+ # you can set the sslmode configuration option via the `PGSSLMODE` environment variable
|
||||
+ # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require)
|
||||
+ # ssl:
|
||||
+ # ca: # if you have a CA file and want to verify it you can uncomment this section
|
||||
+ # $file: <file-path>/ca/server.crt
|
||||
```
|
||||
|
||||
### Using a single database
|
||||
|
||||
@@ -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
|
||||
|
||||