Merge pull request #16664 from schultzp2020/update-codeblocks
Add titles to codeblocks and switch from diff codeblock to language codeblock
This commit is contained in:
@@ -38,7 +38,7 @@ add the provider itself.
|
||||
Add a `providerFactories` entry to the router in
|
||||
`packages/backend/plugin/auth.ts`.
|
||||
|
||||
```ts
|
||||
```ts title="packages/backend/plugin/auth.ts"
|
||||
import { providers } from '@backstage/plugin-auth-backend';
|
||||
|
||||
export default async function createPlugin(
|
||||
@@ -89,12 +89,18 @@ the user's behalf.
|
||||
It is recommended to use the `ProxiedSignInPage` for this provider, which is
|
||||
installed in `packages/app/src/App.tsx` like this:
|
||||
|
||||
```diff
|
||||
+import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
+ SignInPage: props => <ProxiedSignInPage {...props} provider="cfaccess" />,
|
||||
const app = createApp({
|
||||
/* highlight-add-start */
|
||||
components: {
|
||||
SignInPage: props => <ProxiedSignInPage {...props} provider="cfaccess" />,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
});
|
||||
```
|
||||
|
||||
See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page to also work smoothly for local development.
|
||||
|
||||
@@ -45,7 +45,7 @@ callbacks in actual code as well as described below.
|
||||
Add a `providerFactories` entry to the router in
|
||||
`packages/backend/src/plugins/auth.ts`.
|
||||
|
||||
```ts
|
||||
```ts title="packages/backend/src/plugins/auth.ts"
|
||||
import { providers } from '@backstage/plugin-auth-backend';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
@@ -99,12 +99,18 @@ sign-in mechanism to poll that endpoint through the IAP, on the user's behalf.
|
||||
It is recommended to use the `ProxiedSignInPage` for this provider, which is
|
||||
installed in `packages/app/src/App.tsx` like this:
|
||||
|
||||
```diff
|
||||
+import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
+ SignInPage: props => <ProxiedSignInPage {...props} provider="gcp-iap" />,
|
||||
const app = createApp({
|
||||
/* highlight-add-start */
|
||||
components: {
|
||||
SignInPage: props => <ProxiedSignInPage {...props} provider="gcp-iap" />,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
});
|
||||
```
|
||||
|
||||
See the [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) section for more information.
|
||||
|
||||
+67
-54
@@ -84,49 +84,58 @@ which takes either a `provider` or `providers` (array) prop of `SignInProviderCo
|
||||
The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`,
|
||||
and can be adapted to any of the built-in providers:
|
||||
|
||||
```diff
|
||||
+ import { githubAuthApiRef } from '@backstage/core-plugin-api';
|
||||
+ import { SignInPage } from '@backstage/core-components';
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-start */
|
||||
import { githubAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { SignInPage } from '@backstage/core-components';
|
||||
/* highlight-add-end */
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
+ components: {
|
||||
+ SignInPage: props => (
|
||||
+ <SignInPage
|
||||
+ {...props}
|
||||
+ auto
|
||||
+ provider={{
|
||||
+ id: 'github-auth-provider',
|
||||
+ title: 'GitHub',
|
||||
+ message: 'Sign in using GitHub',
|
||||
+ apiRef: githubAuthApiRef,
|
||||
+ }}
|
||||
+ />
|
||||
+ ),
|
||||
+ },
|
||||
bindRoutes({ bind }) {
|
||||
const app = createApp({
|
||||
/* highlight-add-start */
|
||||
components: {
|
||||
SignInPage: props => (
|
||||
<SignInPage
|
||||
{...props}
|
||||
auto
|
||||
provider={{
|
||||
id: 'github-auth-provider',
|
||||
title: 'GitHub',
|
||||
message: 'Sign in using GitHub',
|
||||
apiRef: githubAuthApiRef,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
});
|
||||
```
|
||||
|
||||
You can also use the `providers` prop to enable multiple sign-in methods, for example
|
||||
allows allowing guest access:
|
||||
|
||||
```diff
|
||||
const app = createApp({
|
||||
apis,
|
||||
+ components: {
|
||||
+ SignInPage: props => (
|
||||
+ <SignInPage
|
||||
+ {...props}
|
||||
+ providers={['guest', {
|
||||
+ id: 'github-auth-provider',
|
||||
+ title: 'GitHub',
|
||||
+ message: 'Sign in using GitHub',
|
||||
+ apiRef: githubAuthApiRef,
|
||||
+ }]}
|
||||
+ />
|
||||
+ ),
|
||||
+ },
|
||||
bindRoutes({ bind }) {
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
/* highlight-add-start */
|
||||
components: {
|
||||
SignInPage: props => (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={[
|
||||
'guest',
|
||||
{
|
||||
id: 'github-auth-provider',
|
||||
title: 'GitHub',
|
||||
message: 'Sign in using GitHub',
|
||||
apiRef: githubAuthApiRef,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
});
|
||||
```
|
||||
|
||||
## Sign-In with Proxy Providers
|
||||
@@ -142,12 +151,12 @@ All the sign-in page needs to do is to call the `/refresh` endpoint of the auth
|
||||
to get the existing session, which is exactly what the `ProxiedSignInPage` does. The only
|
||||
thing you need to do to configure the `ProxiedSignInPage` is to pass the ID of the provider like this:
|
||||
|
||||
```tsx
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
...,
|
||||
components: {
|
||||
SignInPage: props => <ProxiedSignInPage {...props} provider="awsalb" />,
|
||||
},
|
||||
// ..
|
||||
});
|
||||
```
|
||||
|
||||
@@ -159,6 +168,7 @@ Example:
|
||||
<ProxiedSignInPage
|
||||
{...props}
|
||||
provider="my-custom-provider"
|
||||
/* highlight-next-line */
|
||||
headers={{ 'x-some-key': someValue }}
|
||||
/>
|
||||
```
|
||||
@@ -169,10 +179,12 @@ Headers can also be returned in an async manner:
|
||||
<ProxiedSignInPage
|
||||
{...props}
|
||||
provider="my-custom-provider"
|
||||
/* highlight-start */
|
||||
headers={async () => {
|
||||
const someValue = await someFn();
|
||||
return { 'x-some-key': someValue };
|
||||
}}
|
||||
/* highlight-end */
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -184,9 +196,8 @@ select the sign-in method based on the `process.env.NODE_ENV` environment variab
|
||||
by checking the `hostname` of the current location, or by accessing the configuration API
|
||||
to read a configuration value. For example:
|
||||
|
||||
```tsx
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
...,
|
||||
components: {
|
||||
SignInPage: props => {
|
||||
const configApi = useApi(configApiRef);
|
||||
@@ -206,6 +217,7 @@ const app = createApp({
|
||||
return <ProxiedSignInPage {...props} provider="gcpiap" />;
|
||||
},
|
||||
},
|
||||
// ..
|
||||
});
|
||||
```
|
||||
|
||||
@@ -218,7 +230,7 @@ If you want to use the authentication capabilities of the [Repository Picker](..
|
||||
|
||||
To set it up, you'll need to add an API factory entry to `packages/app/src/apis.ts`. The example below sets up the `ScmAuthApi` for an already configured GitLab authentication provider:
|
||||
|
||||
```ts
|
||||
```ts title="packages/app/src/apis.ts"
|
||||
createApiFactory({
|
||||
api: scmAuthApiRef,
|
||||
deps: {
|
||||
@@ -294,23 +306,24 @@ If you require only a subset of these integrations, then you will need a custom
|
||||
|
||||
The first step is to remove the code that creates the default providers.
|
||||
|
||||
```diff
|
||||
import {
|
||||
ScmIntegrationsApi,
|
||||
scmIntegrationsApiRef,
|
||||
+ ScmAuth,
|
||||
} from '@backstage/integration-react';
|
||||
```ts title="packages/app/src/apis.ts"
|
||||
import {
|
||||
ScmIntegrationsApi,
|
||||
scmIntegrationsApiRef,
|
||||
/* highlight-add-next-line */
|
||||
ScmAuth,
|
||||
} from '@backstage/integration-react';
|
||||
|
||||
export const apis: AnyApiFactory[] = [
|
||||
...
|
||||
+ ScmAuth.createDefaultApiFactory(),
|
||||
...
|
||||
];
|
||||
export const apis: AnyApiFactory[] = [
|
||||
/* highlight-add-next-line */
|
||||
ScmAuth.createDefaultApiFactory(),
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
Then replace it with something like this, which will create an `ApiFactory` with only a github provider.
|
||||
|
||||
```ts
|
||||
```ts title="packages/app/src/apis.ts"
|
||||
export const apis: AnyApiFactory[] = [
|
||||
createApiFactory({
|
||||
api: scmAuthApiRef,
|
||||
|
||||
@@ -60,12 +60,20 @@ sign-in mechanism to poll that endpoint through the IAP, on the user's behalf.
|
||||
It is recommended to use the `ProxiedSignInPage` for this provider, which is
|
||||
installed in `packages/app/src/App.tsx` like this:
|
||||
|
||||
```diff
|
||||
+import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
+ SignInPage: props => <ProxiedSignInPage {...props} provider="azure-easyauth" />,
|
||||
const app = createApp({
|
||||
/* highlight-add-start */
|
||||
components: {
|
||||
SignInPage: props => (
|
||||
<ProxiedSignInPage {...props} provider="azure-easyauth" />
|
||||
),
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
});
|
||||
```
|
||||
|
||||
See the [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) section for more information.
|
||||
|
||||
@@ -18,7 +18,7 @@ for more details check this
|
||||
The provider configuration can be added to your `app-config.yaml` under the root
|
||||
`auth` configuration:
|
||||
|
||||
```yaml
|
||||
```yaml title="app-config.yaml"
|
||||
auth:
|
||||
providers:
|
||||
oauth2Proxy: {}
|
||||
@@ -63,12 +63,20 @@ providerFactories: {
|
||||
It is recommended to use the `ProxiedSignInPage` for this provider, which is
|
||||
installed in `packages/app/src/App.tsx` like this:
|
||||
|
||||
```diff
|
||||
+import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
+ SignInPage: props => <ProxiedSignInPage {...props} provider="oauth2Proxy" />,
|
||||
const app = createApp({
|
||||
/* highlight-add-start */
|
||||
components: {
|
||||
SignInPage: props => (
|
||||
<ProxiedSignInPage {...props} provider="oauth2Proxy" />
|
||||
),
|
||||
},
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
});
|
||||
```
|
||||
|
||||
See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page to also work smoothly for local development.
|
||||
|
||||
+57
-50
@@ -77,35 +77,35 @@ 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';
|
||||
```ts title="packages/app/src/apis.ts"
|
||||
/* highlight-add-next-line */
|
||||
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',
|
||||
+ ],
|
||||
+ }),
|
||||
+ }),
|
||||
|
||||
/* highlight-add-start */
|
||||
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'],
|
||||
}),
|
||||
}),
|
||||
/* highlight-add-end */
|
||||
// ..
|
||||
];
|
||||
```
|
||||
|
||||
Please note we're importing the `OAuth2` class from `@backstage/core-app-api` effectively
|
||||
@@ -125,7 +125,7 @@ the ID you picked to represent the Auth provider, this ID has to match with the
|
||||
callback URI provider segment (you'll have to configure your IDP to handle the callback
|
||||
URI properly).
|
||||
|
||||
```diff
|
||||
```ts
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
@@ -137,9 +137,11 @@ export default async function createPlugin(
|
||||
tokenManager: env.tokenManager,
|
||||
providerFactories: {
|
||||
...defaultAuthProviderFactories,
|
||||
+ 'my-auth-provider': providers.oidc.create({
|
||||
+ }),
|
||||
}
|
||||
/* highlight-add-next-line */
|
||||
'my-auth-provider': providers.oidc.create({}),
|
||||
},
|
||||
// ..
|
||||
})
|
||||
```
|
||||
|
||||
### The Resolver
|
||||
@@ -154,10 +156,11 @@ 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
|
||||
```ts
|
||||
import {
|
||||
DEFAULT_NAMESPACE,
|
||||
+ stringifyEntityRef,
|
||||
/* highlight-add-next-line */
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
export default async function createPlugin(
|
||||
@@ -172,23 +175,27 @@ export default async function createPlugin(
|
||||
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
|
||||
+ },
|
||||
+ });
|
||||
+ },
|
||||
+ },
|
||||
/* highlight-add-start */
|
||||
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
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
/* highlight-add-end */
|
||||
}),
|
||||
}
|
||||
},
|
||||
// ..
|
||||
})
|
||||
```
|
||||
|
||||
### The configuration
|
||||
@@ -206,7 +213,7 @@ Then we need to configure the env variables for the provider, based on the provi
|
||||
in `plugins/auth-backend/src/providers/oidc/provider.ts` we need the following variables
|
||||
in the `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
```yaml title="app-config.yaml"
|
||||
auth:
|
||||
environment: development
|
||||
### Providing an auth.session.secret will enable session support in the auth-backend
|
||||
|
||||
@@ -23,9 +23,11 @@ that incoming requests are not validated. If you want to enable
|
||||
service-to-service auth, the first step is to switch out the following line in
|
||||
your backend setup at `packages/backend/src/index.ts`:
|
||||
|
||||
```diff
|
||||
- const tokenManager = ServerTokenManager.noop();
|
||||
+ const tokenManager = ServerTokenManager.fromConfig(config, { logger: root });
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-remove-next-line */
|
||||
const tokenManager = ServerTokenManager.noop();
|
||||
/* highlight-add-next-line */
|
||||
const tokenManager = ServerTokenManager.fromConfig(config, { logger: root });
|
||||
```
|
||||
|
||||
By switching from the no-op `ServiceTokenManager` to one created from config,
|
||||
|
||||
@@ -72,8 +72,7 @@ comment out its old contents, or renaming the old file to `index.backup.ts` for
|
||||
reference and making a new blank one to work on - whichever works best for you.
|
||||
These are our new blank contents in the index file:
|
||||
|
||||
```ts
|
||||
// packages/backend/src/index.ts
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
|
||||
const backend = createBackend();
|
||||
@@ -95,13 +94,15 @@ backend start` command locally and seeing some logs scroll by. But it'll just be
|
||||
a blank service with no real features added. So let's stop it with `Ctrl+C` and
|
||||
reintroduce some plugins into the mix.
|
||||
|
||||
```diff
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
+import { legacyPlugin } from '@backstage/backend-common';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
/* highlight-add-next-line */
|
||||
import { legacyPlugin } from '@backstage/backend-common';
|
||||
|
||||
const backend = createBackend();
|
||||
+backend.add(legacyPlugin('todo', import('./plugins/todo')));
|
||||
backend.start();
|
||||
const backend = createBackend();
|
||||
/* highlight-add-next-line */
|
||||
backend.add(legacyPlugin('todo', import('./plugins/todo')));
|
||||
backend.start();
|
||||
```
|
||||
|
||||
The `todo` plugin used above is just an example and you may not have it enabled
|
||||
@@ -128,34 +129,42 @@ environment was changed from the defaults, perhaps with your own custom
|
||||
additions. If this is the case in your installation, you still aren't out of
|
||||
luck - you can build a customized `legacyPlugin` function.
|
||||
|
||||
```diff
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
-import { legacyPlugin } from '@backstage/backend-common';
|
||||
+import { makeLegacyPlugin, loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
+import { coreServices } from '@backstage/backend-plugin-api';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
/* highlight-remove-next-line */
|
||||
import { legacyPlugin } from '@backstage/backend-common';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
makeLegacyPlugin,
|
||||
loggerToWinstonLogger,
|
||||
} from '@backstage/backend-common';
|
||||
import { coreServices } from '@backstage/backend-plugin-api';
|
||||
/* highlight-add-end */
|
||||
|
||||
+const legacyPlugin = makeLegacyPlugin(
|
||||
+ {
|
||||
+ cache: coreServices.cache,
|
||||
+ config: coreServices.config,
|
||||
+ database: coreServices.database,
|
||||
+ discovery: coreServices.discovery,
|
||||
+ logger: coreServices.logger,
|
||||
+ permissions: coreServices.permissions,
|
||||
+ scheduler: coreServices.scheduler,
|
||||
+ tokenManager: coreServices.tokenManager,
|
||||
+ reader: coreServices.urlReader,
|
||||
+ identity: coreServices.identity,
|
||||
+ // ... and your own additions
|
||||
+ },
|
||||
+ {
|
||||
+ logger: log => loggerToWinstonLogger(log),
|
||||
+ },
|
||||
+);
|
||||
/* highlight-add-start */
|
||||
const legacyPlugin = makeLegacyPlugin(
|
||||
{
|
||||
cache: coreServices.cache,
|
||||
config: coreServices.config,
|
||||
database: coreServices.database,
|
||||
discovery: coreServices.discovery,
|
||||
logger: coreServices.logger,
|
||||
permissions: coreServices.permissions,
|
||||
scheduler: coreServices.scheduler,
|
||||
tokenManager: coreServices.tokenManager,
|
||||
reader: coreServices.urlReader,
|
||||
identity: coreServices.identity,
|
||||
// ... and your own additions
|
||||
},
|
||||
{
|
||||
logger: log => loggerToWinstonLogger(log),
|
||||
},
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(legacyPlugin('todo', import('./plugins/todo')));
|
||||
backend.start();
|
||||
const backend = createBackend();
|
||||
backend.add(legacyPlugin('todo', import('./plugins/todo')));
|
||||
backend.start();
|
||||
```
|
||||
|
||||
The first argument to `makeLegacyPlugin` is the mapping from environment keys to
|
||||
@@ -177,18 +186,20 @@ in question.
|
||||
In this example, we'll assume that your added environment field is named
|
||||
`example`, and the created ref is named `exampleServiceRef`.
|
||||
|
||||
```diff
|
||||
+import { exampleServiceRef } from '<somewhere>'; // if the definition is elsewhere
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { exampleServiceRef } from '<somewhere>'; // if the definition is elsewhere
|
||||
|
||||
const legacyPlugin = makeLegacyPlugin(
|
||||
{
|
||||
// ... the above core services still go here
|
||||
+ example: exampleServiceRef
|
||||
},
|
||||
{
|
||||
logger: log => loggerToWinstonLogger(log),
|
||||
},
|
||||
);
|
||||
const legacyPlugin = makeLegacyPlugin(
|
||||
{
|
||||
// ... the above core services still go here
|
||||
/* highlight-add-next-line */
|
||||
example: exampleServiceRef,
|
||||
},
|
||||
{
|
||||
logger: log => loggerToWinstonLogger(log),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
After this, your backend will know how to instantiate your thing on demand and
|
||||
@@ -220,12 +231,13 @@ such migrations you can make.
|
||||
The app backend plugin that serves the frontend from the backend can trivially
|
||||
be used in its new form.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
+import { appPlugin } from '@backstage/plugin-app-backend';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { appPlugin } from '@backstage/plugin-app-backend';
|
||||
|
||||
const backend = createBackend();
|
||||
+backend.add(appPlugin({ appPackageName: 'app' }));
|
||||
const backend = createBackend();
|
||||
/* highlight-add-next-line */
|
||||
backend.add(appPlugin({ appPackageName: 'app' }));
|
||||
```
|
||||
|
||||
This is an example of how options can be passed into some backend plugins. The
|
||||
@@ -239,14 +251,17 @@ You should be able to delete the `plugins/app.ts` file at this point.
|
||||
|
||||
A basic installation of the catalog plugin looks as follows.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
+import { catalogPlugin } from '@backstage/plugin-catalog-backend';
|
||||
+import { catalogModuleTemplateKind } from '@backstage/plugin-scaffolder-backend';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-start */
|
||||
import { catalogPlugin } from '@backstage/plugin-catalog-backend';
|
||||
import { catalogModuleTemplateKind } from '@backstage/plugin-scaffolder-backend';
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
+backend.add(catalogPlugin());
|
||||
+backend.add(catalogModuleTemplateKind());
|
||||
const backend = createBackend();
|
||||
/* highlight-add-start */
|
||||
backend.add(catalogPlugin());
|
||||
backend.add(catalogModuleTemplateKind());
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
Note that this also installs a module from the scaffolder, namely the one which
|
||||
@@ -262,34 +277,38 @@ mechanism to extend or tweak the functionality of the plugin. To do that,
|
||||
you'll make your own bespoke [module](../architecture/06-modules.md) which
|
||||
depends on the appropriate extension point and interacts with it.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
+import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
|
||||
+import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-start */
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
/* highlight-add-end */
|
||||
|
||||
+const catalogModuleCustomExtensions = createBackendModule({
|
||||
+ pluginId: 'catalog', // name of the plugin that the module is targeting
|
||||
+ moduleId: 'customExtensions',
|
||||
+ register(env) {
|
||||
+ env.registerInit({
|
||||
+ deps: {
|
||||
+ catalog: catalogProcessingExtensionPoint,
|
||||
+ // ... and other dependencies as needed
|
||||
+ },
|
||||
+ init({ catalog /* ..., other dependencies */ }) {
|
||||
+ // Here you have the opportunity to interact with the extension
|
||||
+ // point before the plugin itself gets instantiated
|
||||
+ catalog.addEntityProvider(new MyEntityProvider()); // just an example
|
||||
+ catalog.addProcessor(new MyProcessor()); // just an example
|
||||
+ },
|
||||
+ });
|
||||
+ },
|
||||
+});
|
||||
/* highlight-add-start */
|
||||
const catalogModuleCustomExtensions = createBackendModule({
|
||||
pluginId: 'catalog', // name of the plugin that the module is targeting
|
||||
moduleId: 'customExtensions',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
// ... and other dependencies as needed
|
||||
},
|
||||
init({ catalog /* ..., other dependencies */ }) {
|
||||
// Here you have the opportunity to interact with the extension
|
||||
// point before the plugin itself gets instantiated
|
||||
catalog.addEntityProvider(new MyEntityProvider()); // just an example
|
||||
catalog.addProcessor(new MyProcessor()); // just an example
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(catalogPlugin());
|
||||
backend.add(catalogModuleTemplateKind());
|
||||
+backend.add(catalogModuleCustomExtensions());
|
||||
const backend = createBackend();
|
||||
backend.add(catalogPlugin());
|
||||
backend.add(catalogModuleTemplateKind());
|
||||
/* highlight-add-next-line */
|
||||
backend.add(catalogModuleCustomExtensions());
|
||||
```
|
||||
|
||||
This also requires that you have a dependency on the corresponding node package,
|
||||
@@ -310,12 +329,13 @@ implementations that they represent, and being exported from there.
|
||||
|
||||
A basic installation of the events plugin looks as follows.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
+import { eventsPlugin } from '@backstage/plugin-events-backend';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { eventsPlugin } from '@backstage/plugin-events-backend';
|
||||
|
||||
const backend = createBackend();
|
||||
+backend.add(eventsPlugin());
|
||||
const backend = createBackend();
|
||||
/* highlight-add-next-line */
|
||||
backend.add(eventsPlugin());
|
||||
```
|
||||
|
||||
If you have other customizations made to `plugins/events.ts`, such as adding
|
||||
@@ -327,32 +347,36 @@ mechanism to extend or tweak the functionality of the plugin. To do that,
|
||||
you'll make your own bespoke [module](../architecture/06-modules.md) which
|
||||
depends on the appropriate extension point and interacts with it.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
+import { eventsExtensionPoint } from '@backstage/plugin-events-node';
|
||||
+import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-start */
|
||||
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
/* highlight-add-end */
|
||||
|
||||
+const eventsModuleCustomExtensions = createBackendModule({
|
||||
+ pluginId: 'events', // name of the plugin that the module is targeting
|
||||
+ moduleId: 'customExtensions',
|
||||
+ register(env) {
|
||||
+ env.registerInit({
|
||||
+ deps: {
|
||||
+ events: eventsExtensionPoint,
|
||||
+ // ... and other dependencies as needed
|
||||
+ },
|
||||
+ init({ events /* ..., other dependencies */ }) {
|
||||
+ // Here you have the opportunity to interact with the extension
|
||||
+ // point before the plugin itself gets instantiated
|
||||
+ events.addSubscribers(new MySubscriber()); // just an example
|
||||
+ },
|
||||
+ });
|
||||
+ },
|
||||
+});
|
||||
/* highlight-add-start */
|
||||
const eventsModuleCustomExtensions = createBackendModule({
|
||||
pluginId: 'events', // name of the plugin that the module is targeting
|
||||
moduleId: 'customExtensions',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
events: eventsExtensionPoint,
|
||||
// ... and other dependencies as needed
|
||||
},
|
||||
init({ events /* ..., other dependencies */ }) {
|
||||
// Here you have the opportunity to interact with the extension
|
||||
// point before the plugin itself gets instantiated
|
||||
events.addSubscribers(new MySubscriber()); // just an example
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(eventsPlugin());
|
||||
+backend.add(eventsModuleCustomExtensions());
|
||||
const backend = createBackend();
|
||||
backend.add(eventsPlugin());
|
||||
/* highlight-add-next-line */
|
||||
backend.add(eventsModuleCustomExtensions());
|
||||
```
|
||||
|
||||
This also requires that you have a dependency on the corresponding node package,
|
||||
@@ -373,12 +397,13 @@ implementations that they represent, and being exported from there.
|
||||
|
||||
A basic installation of the scaffolder plugin looks as follows.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
+import { scaffolderPlugin } from '@backstage/plugin-scaffolder-backend';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { scaffolderPlugin } from '@backstage/plugin-scaffolder-backend';
|
||||
|
||||
const backend = createBackend();
|
||||
+backend.add(scaffolderPlugin());
|
||||
const backend = createBackend();
|
||||
/* highlight-add-next-line */
|
||||
backend.add(scaffolderPlugin());
|
||||
```
|
||||
|
||||
If you have other customizations made to `plugins/scaffolder.ts`, such as adding
|
||||
@@ -390,32 +415,36 @@ mechanism to extend or tweak the functionality of the plugin. To do that,
|
||||
you'll make your own bespoke [module](../architecture/06-modules.md) which
|
||||
depends on the appropriate extension point and interacts with it.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
+import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node';
|
||||
+import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-start */
|
||||
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
/* highlight-add-end */
|
||||
|
||||
+const scaffolderModuleCustomExtensions = createBackendModule({
|
||||
+ pluginId: 'scaffolder', // name of the plugin that the module is targeting
|
||||
+ moduleId: 'customExtensions',
|
||||
+ register(env) {
|
||||
+ env.registerInit({
|
||||
+ deps: {
|
||||
+ scaffolder: scaffolderActionsExtensionPoint,
|
||||
+ // ... and other dependencies as needed
|
||||
+ },
|
||||
+ init({ scaffolder /* ..., other dependencies */ }) {
|
||||
+ // Here you have the opportunity to interact with the extension
|
||||
+ // point before the plugin itself gets instantiated
|
||||
+ scaffolder.addActions(new MyAction()); // just an example
|
||||
+ },
|
||||
+ });
|
||||
+ },
|
||||
+});
|
||||
/* highlight-add-start */
|
||||
const scaffolderModuleCustomExtensions = createBackendModule({
|
||||
pluginId: 'scaffolder', // name of the plugin that the module is targeting
|
||||
moduleId: 'customExtensions',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
scaffolder: scaffolderActionsExtensionPoint,
|
||||
// ... and other dependencies as needed
|
||||
},
|
||||
init({ scaffolder /* ..., other dependencies */ }) {
|
||||
// Here you have the opportunity to interact with the extension
|
||||
// point before the plugin itself gets instantiated
|
||||
scaffolder.addActions(new MyAction()); // just an example
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(scaffolderPlugin());
|
||||
+backend.add(scaffolderModuleCustomExtensions());
|
||||
const backend = createBackend();
|
||||
backend.add(scaffolderPlugin());
|
||||
/* highlight-add-next-line */
|
||||
backend.add(scaffolderModuleCustomExtensions());
|
||||
```
|
||||
|
||||
This also requires that you have a dependency on the corresponding node package,
|
||||
|
||||
@@ -23,8 +23,8 @@ yarn add --cwd packages/app @backstage/plugin-kubernetes
|
||||
Once the package has been installed, you need to import the plugin in your app
|
||||
by adding the "Kubernetes" tab to the respective catalog pages.
|
||||
|
||||
```tsx
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
|
||||
|
||||
// You can add the tab to any number of pages, the service page is shown as an
|
||||
@@ -32,9 +32,13 @@ import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
|
||||
const serviceEntityPage = (
|
||||
<EntityLayout>
|
||||
{/* other tabs... */}
|
||||
{/* highlight-add-start */}
|
||||
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
|
||||
<EntityKubernetesContent refreshIntervalMs={30000} />
|
||||
</EntityLayout.Route>
|
||||
{/* highlight-add-end */}
|
||||
</EntityLayout>
|
||||
);
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
@@ -57,8 +61,7 @@ yarn add --cwd packages/backend @backstage/plugin-kubernetes-backend
|
||||
Create a file called `kubernetes.ts` inside `packages/backend/src/plugins/` and
|
||||
add the following:
|
||||
|
||||
```typescript
|
||||
// In packages/backend/src/plugins/kubernetes.ts
|
||||
```ts title="packages/backend/src/plugins/kubernetes.ts"
|
||||
import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
@@ -81,14 +84,17 @@ And import the plugin to `packages/backend/src/index.ts`. There are three lines
|
||||
of code you'll need to add, and they should be added near similar code in your
|
||||
existing Backstage backend.
|
||||
|
||||
```typescript
|
||||
// In packages/backend/src/index.ts
|
||||
```typescript title="packages/backend/src/index.ts"
|
||||
// ..
|
||||
/* highlight-add-next-line */
|
||||
import kubernetes from './plugins/kubernetes';
|
||||
// ...
|
||||
|
||||
async function main() {
|
||||
// ...
|
||||
/* highlight-add-next-line */
|
||||
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
|
||||
// ...
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
|
||||
```
|
||||
|
||||
@@ -104,52 +110,66 @@ don't work for your use-case, it is possible to implement a custom
|
||||
|
||||
Change the following in `packages/backend/src/plugins/kubernetes.ts`:
|
||||
|
||||
```diff
|
||||
-import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
|
||||
+import {
|
||||
+ ClusterDetails,
|
||||
+ KubernetesBuilder,
|
||||
+ KubernetesClustersSupplier,
|
||||
+} from '@backstage/plugin-kubernetes-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
+import { Duration } from 'luxon';
|
||||
+
|
||||
+export class CustomClustersSupplier implements KubernetesClustersSupplier {
|
||||
+ constructor(private clusterDetails: ClusterDetails[] = []) {}
|
||||
+
|
||||
+ static create(refreshInterval: Duration) {
|
||||
+ const clusterSupplier = new CustomClustersSupplier();
|
||||
+ // setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts
|
||||
+ runPeriodically(
|
||||
+ () => clusterSupplier.refreshClusters(),
|
||||
+ refreshInterval.toMillis(),
|
||||
+ );
|
||||
+ return clusterSupplier;
|
||||
+ }
|
||||
+
|
||||
+ async refreshClusters(): Promise<void> {
|
||||
+ this.clusterDetails = []; // fetch from somewhere
|
||||
+ }
|
||||
+
|
||||
+ async getClusters(): Promise<ClusterDetails[]> {
|
||||
+ return this.clusterDetails;
|
||||
+ }
|
||||
+}
|
||||
```ts title="packages/backend/src/plugins/kubernetes.ts"
|
||||
import {
|
||||
/* highlight-add-next-line */
|
||||
ClusterDetails,
|
||||
KubernetesBuilder,
|
||||
/* highlight-add-next-line */
|
||||
KubernetesClustersSupplier,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
/* highlight-add-next-line */
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
- const { router } = await KubernetesBuilder.createBuilder({
|
||||
+ const builder = await KubernetesBuilder.createBuilder({
|
||||
/* highlight-add-start */
|
||||
export class CustomClustersSupplier implements KubernetesClustersSupplier {
|
||||
constructor(private clusterDetails: ClusterDetails[] = []) {}
|
||||
|
||||
static create(refreshInterval: Duration) {
|
||||
const clusterSupplier = new CustomClustersSupplier();
|
||||
// setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts
|
||||
runPeriodically(
|
||||
() => clusterSupplier.refreshClusters(),
|
||||
refreshInterval.toMillis(),
|
||||
);
|
||||
return clusterSupplier;
|
||||
}
|
||||
|
||||
async refreshClusters(): Promise<void> {
|
||||
this.clusterDetails = []; // fetch from somewhere
|
||||
}
|
||||
|
||||
async getClusters(): Promise<ClusterDetails[]> {
|
||||
return this.clusterDetails;
|
||||
}
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
|
||||
/* highlight-remove-next-line */
|
||||
const { router } = await KubernetesBuilder.createBuilder({
|
||||
/* highlight-add-next-line */
|
||||
const builder = await KubernetesBuilder.createBuilder({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
- }).build();
|
||||
+ });
|
||||
+ builder.setClusterSupplier(
|
||||
+ CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })),
|
||||
+ );
|
||||
+ const { router } = await builder.build();
|
||||
/* highlight-remove-next-line */
|
||||
}).build();
|
||||
/* highlight-add-start */
|
||||
});
|
||||
builder.setClusterSupplier(
|
||||
CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })),
|
||||
);
|
||||
const { router } = await builder.build();
|
||||
/* highlight-add-end */
|
||||
|
||||
// ..
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
## Running Backstage locally
|
||||
|
||||
@@ -20,30 +20,30 @@ to do that in two steps.
|
||||
[interface](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L31)
|
||||
according to your needs.
|
||||
|
||||
```typescript
|
||||
export class SearchClient implements SearchApi {
|
||||
// your implementation
|
||||
}
|
||||
```
|
||||
```typescript
|
||||
export class SearchClient implements SearchApi {
|
||||
// your implementation
|
||||
}
|
||||
```
|
||||
|
||||
2. Override the API ref `searchApiRef` with your new implemented API in the
|
||||
`App.tsx` using `ApiFactories`.
|
||||
[Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis).
|
||||
|
||||
```typescript
|
||||
const app = createApp({
|
||||
apis: [
|
||||
// SearchApi
|
||||
createApiFactory({
|
||||
api: searchApiRef,
|
||||
deps: { discovery: discoveryApiRef },
|
||||
factory({ discovery }) {
|
||||
return new SearchClient({ discoveryApi: discovery });
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
```typescript
|
||||
const app = createApp({
|
||||
apis: [
|
||||
// SearchApi
|
||||
createApiFactory({
|
||||
api: searchApiRef,
|
||||
deps: { discovery: discoveryApiRef },
|
||||
factory({ discovery }) {
|
||||
return new SearchClient({ discoveryApi: discovery });
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## How to index TechDocs documents
|
||||
|
||||
@@ -63,35 +63,35 @@ getting started guide.
|
||||
1. Import the `DefaultTechDocsCollatorFactory` from
|
||||
`@backstage/plugin-techdocs-backend`.
|
||||
|
||||
```typescript
|
||||
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
|
||||
```
|
||||
```typescript
|
||||
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
|
||||
```
|
||||
|
||||
2. If there isn't an existing schedule you'd like to run the collator on, be
|
||||
sure to create it first. Something like...
|
||||
|
||||
```typescript
|
||||
import { Duration } from 'luxon';
|
||||
```typescript
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
|
||||
frequency: Duration.fromObject({ seconds: 600 }),
|
||||
timeout: Duration.fromObject({ seconds: 900 }),
|
||||
initialDelay: Duration.fromObject({ seconds: 3 }),
|
||||
});
|
||||
```
|
||||
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
|
||||
frequency: Duration.fromObject({ seconds: 600 }),
|
||||
timeout: Duration.fromObject({ seconds: 900 }),
|
||||
initialDelay: Duration.fromObject({ seconds: 3 }),
|
||||
});
|
||||
```
|
||||
|
||||
3. Register the `DefaultTechDocsCollatorFactory` with the IndexBuilder.
|
||||
|
||||
```typescript
|
||||
indexBuilder.addCollator({
|
||||
schedule: every10MinutesSchedule,
|
||||
factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
|
||||
discovery: env.discovery,
|
||||
logger: env.logger,
|
||||
tokenManager: env.tokenManager,
|
||||
}),
|
||||
});
|
||||
```
|
||||
```typescript
|
||||
indexBuilder.addCollator({
|
||||
schedule: every10MinutesSchedule,
|
||||
factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
|
||||
discovery: env.discovery,
|
||||
logger: env.logger,
|
||||
tokenManager: env.tokenManager,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
You should now have your TechDocs documents indexed to your search engine of
|
||||
choice!
|
||||
@@ -101,14 +101,14 @@ searching, you can update your `SearchPage.tsx` file in
|
||||
`packages/app/src/components/search` by adding `techdocs` to the list of values
|
||||
of the `SearchType` component.
|
||||
|
||||
```tsx
|
||||
```tsx title="packages/app/src/components/search/SearchPage.tsx"
|
||||
<Paper className={classes.filters}>
|
||||
<SearchType
|
||||
values={['techdocs', 'software-catalog']}
|
||||
name="type"
|
||||
defaultValue="software-catalog"
|
||||
/>
|
||||
...
|
||||
{/* ... */}
|
||||
</Paper>
|
||||
```
|
||||
|
||||
@@ -124,10 +124,10 @@ You can either just simply amend default behaviour, or even to write completely
|
||||
|
||||
> `authorization` and `location` cannot be modified via a `entityTransformer`, `location` can be modified only through `locationTemplate`.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/search.ts
|
||||
|
||||
const entityTransformer: CatalogCollatorEntityTransformer = (entity: Entity) => {
|
||||
```ts title="packages/backend/src/plugins/search.ts"
|
||||
const entityTransformer: CatalogCollatorEntityTransformer = (
|
||||
entity: Entity,
|
||||
) => {
|
||||
if (entity.kind === 'SomeKind') {
|
||||
return {
|
||||
// customize here output for 'SomeKind' kind
|
||||
@@ -145,7 +145,8 @@ indexBuilder.addCollator({
|
||||
collator: DefaultCatalogCollatorFactory.fromConfig(env.config, {
|
||||
discovery: env.discovery,
|
||||
tokenManager: env.tokenManager,
|
||||
+ entityTransformer,
|
||||
/* highlight-add-next-line */
|
||||
entityTransformer,
|
||||
}),
|
||||
});
|
||||
```
|
||||
@@ -166,17 +167,17 @@ exactly what's available to search, (or a [Decorator](./concepts.md#decorators)
|
||||
to filter things out here and there), but the `DefaultCatalogCollator` that's
|
||||
provided by `@backstage/plugin-catalog-backend` offers some configuration too!
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/search.ts
|
||||
|
||||
```ts title="packages/backend/src/plugins/search.ts"
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: DefaultCatalogCollator.fromConfig(env.config, {
|
||||
discovery: env.discovery,
|
||||
tokenManager: env.tokenManager,
|
||||
+ filter: {
|
||||
+ kind: ['API', 'Component', 'Domain', 'Group', 'System', 'User'],
|
||||
+ },
|
||||
/* highlight-add-start */
|
||||
filter: {
|
||||
kind: ['API', 'Component', 'Domain', 'Group', 'System', 'User'],
|
||||
},
|
||||
/* highlight-add-end */
|
||||
}),
|
||||
});
|
||||
```
|
||||
@@ -194,7 +195,7 @@ to create an override with your preferred styling.
|
||||
|
||||
For example, the following will result in highlighted terms to be bold & underlined:
|
||||
|
||||
```jsx
|
||||
```tsx
|
||||
const highlightOverride = {
|
||||
BackstageHighlightedSearchResultText: {
|
||||
highlight: {
|
||||
@@ -207,10 +208,6 @@ const highlightOverride = {
|
||||
};
|
||||
```
|
||||
|
||||
[obj-mode]: https://nodejs.org/dist/latest-v16.x/docs/api/stream.html#stream_object_mode
|
||||
[read-stream]: https://nodejs.org/dist/latest-v16.x/docs/api/stream.html#readable-streams
|
||||
[async-gen]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#iterating_over_async_generators
|
||||
|
||||
## How to render search results using extensions
|
||||
|
||||
Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages:
|
||||
@@ -219,8 +216,7 @@ Extensions for search results let you customize components used to render search
|
||||
|
||||
Using the example below, you can provide an extension to be used as a default result item:
|
||||
|
||||
```tsx
|
||||
// plugins/your-plugin/src/plugin.ts
|
||||
```tsx title="plugins/your-plugin/src/plugin.ts"
|
||||
import { createPlugin } from '@backstage/core-plugin-api';
|
||||
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
|
||||
|
||||
@@ -251,8 +247,7 @@ export const YourSearchResultListItemExtension: (
|
||||
|
||||
Additionally, you can define a predicate function that receives a result and returns whether your extension should be used to render it or not:
|
||||
|
||||
```tsx
|
||||
// plugins/your-plugin/src/plugin.ts
|
||||
```tsx title="plugins/your-plugin/src/plugin.ts"
|
||||
import { createPlugin } from '@backstage/core-plugin-api';
|
||||
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
|
||||
|
||||
@@ -271,8 +266,7 @@ export const YourSearchResultListItemExtension = plugin.provide(
|
||||
|
||||
Remember to export your new extension:
|
||||
|
||||
```tsx
|
||||
// plugins/your-plugin/src/index.ts
|
||||
```tsx title="plugins/your-plugin/src/index.ts"
|
||||
export { YourSearchResultListItem } from './plugin.ts';
|
||||
```
|
||||
|
||||
@@ -282,8 +276,7 @@ For more details, see the [createSearchResultListItemExtension](https://backstag
|
||||
|
||||
Now that you know how a search result item is provided, let's finally see how they can be used, for example, to compose a page in your application:
|
||||
|
||||
```tsx
|
||||
// packages/app/src/components/searchPage.tsx
|
||||
```tsx title="packages/app/src/components/searchPage.tsx"
|
||||
import React from 'react';
|
||||
|
||||
import { Grid, Paper } from '@material-ui/core';
|
||||
@@ -337,8 +330,7 @@ export const searchPage = <SearchPage />;
|
||||
|
||||
As another example, here's a search modal that renders results with extensions:
|
||||
|
||||
```tsx
|
||||
// packages/app/src/components/searchModal.tsx
|
||||
```tsx title="packages/app/src/components/searchModal.tsx"
|
||||
import React from 'react';
|
||||
|
||||
import { DialogContent, DialogTitle, Paper } from '@material-ui/core';
|
||||
@@ -367,7 +359,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => (
|
||||
<CatalogSearchResultListItem icon={<CatalogIcon />} />
|
||||
<TechDocsSearchResultListItem icon={<DocsIcon />} />
|
||||
<ToolSearchResultListItem icon={<BuildIcon />} />
|
||||
{/* As a "default" extension, it does not define a predicate function,
|
||||
{/* As a "default" extension, it does not define a predicate function,
|
||||
so it must be the last child to render results that do not match the above extensions */}
|
||||
<YourSearchResultListItem />
|
||||
</SearchResult>
|
||||
|
||||
@@ -142,21 +142,22 @@ export const EntitySecurityTierPicker = () => {
|
||||
|
||||
Now we can add the component to `CustomCatalogPage`:
|
||||
|
||||
```diff
|
||||
```tsx
|
||||
export const CustomCatalogPage = ({
|
||||
columns,
|
||||
actions,
|
||||
initiallySelectedFilter = 'owned',
|
||||
}: CatalogPageProps) => {
|
||||
return (
|
||||
...
|
||||
{/* ... */}
|
||||
<EntityListProvider>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<EntityKindPicker initialFilter="component" hidden />
|
||||
<EntityTypePicker />
|
||||
<UserListPicker initialFilter={initiallySelectedFilter} />
|
||||
+ <EntitySecurityTierPicker />
|
||||
{/* highlight-add-next-line */}
|
||||
<EntitySecurityTierPicker />
|
||||
<EntityTagPicker />
|
||||
<CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
@@ -164,21 +165,26 @@ export const CustomCatalogPage = ({
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</EntityListProvider>
|
||||
...
|
||||
{/* ... */}
|
||||
};
|
||||
```
|
||||
|
||||
Finally, we can apply our new `CustomCatalogPage`.
|
||||
|
||||
```diff
|
||||
# packages/app/src/App.tsx
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Navigate key="/" to="catalog" />
|
||||
- <Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
+ <Route path="/catalog" element={<CatalogIndexPage />}>
|
||||
+ <CustomCatalogPage />
|
||||
+ </Route>
|
||||
{/* highlight-remove-next-line */}
|
||||
<Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
{/* highlight-add-start */}
|
||||
<Route path="/catalog" element={<CatalogIndexPage />}>
|
||||
<CustomCatalogPage />
|
||||
</Route>
|
||||
{/* highlight-add-end */}
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
The same method can be used to customize the _default_ filters with a different
|
||||
|
||||
@@ -530,12 +530,17 @@ export class FoobarEntitiesProcessor implements CatalogProcessor {
|
||||
Once the processor is created it can be wired up to the catalog via the
|
||||
`CatalogBuilder` in `packages/backend/src/plugins/catalog.ts`:
|
||||
|
||||
```diff
|
||||
+ import { FoobarEntitiesProcessor } from '@internal/plugin-foobar-backend';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { FoobarEntitiesProcessor } from '@internal/plugin-foobar-backend';
|
||||
|
||||
// ...
|
||||
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
+ builder.addProcessor(new FoobarEntitiesProcessor());
|
||||
const { processingEngine, router } = await builder.build();
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-next-line */
|
||||
builder.addProcessor(new FoobarEntitiesProcessor());
|
||||
const { processingEngine, router } = await builder.build();
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
@@ -235,26 +235,35 @@ others.
|
||||
You should now be able to add this class to your backend in
|
||||
`packages/backend/src/plugins/catalog.ts`:
|
||||
|
||||
```diff
|
||||
+import { FrobsProvider } from '../path/to/class';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { FrobsProvider } from '../path/to/class';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = CatalogBuilder.create(env);
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = CatalogBuilder.create(env);
|
||||
/* highlight-add-start */
|
||||
const frobs = new FrobsProvider('production', env.reader);
|
||||
builder.addEntityProvider(frobs);
|
||||
/* highlight-add-end */
|
||||
|
||||
+ const frobs = new FrobsProvider('production', env.reader);
|
||||
+ builder.addEntityProvider(frobs);
|
||||
const { processingEngine, router } = await builder.build();
|
||||
await processingEngine.start();
|
||||
|
||||
const { processingEngine, router } = await builder.build();
|
||||
await processingEngine.start();
|
||||
/* highlight-add-start */
|
||||
await env.scheduler.scheduleTask({
|
||||
id: 'run_frobs_refresh',
|
||||
fn: async () => {
|
||||
await frobs.run();
|
||||
},
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 10 },
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
+ await env.scheduler.scheduleTask({
|
||||
+ id: 'run_frobs_refresh',
|
||||
+ fn: async () => { await frobs.run(); },
|
||||
+ frequency: { minutes: 30 },
|
||||
+ timeout: { minutes: 10 },
|
||||
+ });
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
Note that we used the builtin scheduler facility to regularly call the `run`
|
||||
@@ -464,8 +473,7 @@ feeding it into the ingestion loop. For this kind of an integration, you'd
|
||||
typically want to add it to the list of statically always-available locations in
|
||||
the config.
|
||||
|
||||
```yaml
|
||||
# In app-config.yaml
|
||||
```yaml title="app-config.yaml"
|
||||
catalog:
|
||||
locations:
|
||||
- type: system-x
|
||||
@@ -551,14 +559,19 @@ The key points to note are:
|
||||
You should now be able to add this class to your backend in
|
||||
`packages/backend/src/plugins/catalog.ts`:
|
||||
|
||||
```diff
|
||||
+import { SystemXReaderProcessor } from '../path/to/class';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { SystemXReaderProcessor } from '../path/to/class';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = CatalogBuilder.create(env);
|
||||
+ builder.addProcessor(new SystemXReaderProcessor(env.reader));
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = CatalogBuilder.create(env);
|
||||
/* highlight-add-next-line */
|
||||
builder.addProcessor(new SystemXReaderProcessor(env.reader));
|
||||
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
Start up the backend - it should now start reading from the previously
|
||||
|
||||
@@ -31,16 +31,19 @@ allow most templates built for `fetch:cookiecutter` to work without any changes.
|
||||
2. Set `cookiecutterCompat` to `true` in the `fetch:template` step input in
|
||||
`template.yaml`.
|
||||
|
||||
```diff
|
||||
steps:
|
||||
- id: fetch-base
|
||||
name: Fetch Base
|
||||
- action: fetch:cookiecutter
|
||||
+ action: fetch:template
|
||||
input:
|
||||
url: ./skeleton
|
||||
+ cookiecutterCompat: true
|
||||
values:
|
||||
```yaml title="template.yaml"
|
||||
steps:
|
||||
- id: fetch-base
|
||||
name: Fetch Base
|
||||
# highlight-remove-next-line
|
||||
action: fetch:cookiecutter
|
||||
# highlight-add-next-line
|
||||
action: fetch:template
|
||||
input:
|
||||
url: ./skeleton
|
||||
# highlight-add-next-line
|
||||
cookiecutterCompat: true
|
||||
values:
|
||||
```
|
||||
|
||||
### Manual migration
|
||||
|
||||
@@ -38,14 +38,20 @@ to upgrade.
|
||||
|
||||
An important change is to add the required processor to your `packages/backend/src/plugins/catalog.ts`
|
||||
|
||||
```diff
|
||||
+import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
|
||||
|
||||
...
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-next-line */
|
||||
builder.addProcessor(new ScaffolderEntitiesProcessor());
|
||||
const { processingEngine, router } = await builder.build();
|
||||
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
+ builder.addProcessor(new ScaffolderEntitiesProcessor());
|
||||
const { processingEngine, router } = await builder.build();
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
## `backstage.io/v1beta2` -> `scaffolder.backstage.io/v1beta3`
|
||||
@@ -53,10 +59,12 @@ An important change is to add the required processor to your `packages/backend/s
|
||||
The most important change is that you'll need to switch over the `apiVersion` in
|
||||
your templates to the new one.
|
||||
|
||||
```diff
|
||||
```yaml
|
||||
kind: Template
|
||||
- apiVersion: backstage.io/v1beta2
|
||||
+ apiVersion: scaffolder.backstage.io/v1beta3
|
||||
# highlight-remove-next-line
|
||||
apiVersion: backstage.io/v1beta2
|
||||
# highlight-add-next-line
|
||||
apiVersion: scaffolder.backstage.io/v1beta3
|
||||
```
|
||||
|
||||
## `${{ }}` instead of `"{{ }}"`
|
||||
@@ -68,15 +76,19 @@ was pretty annoying, as it also meant that all things look like strings. Now
|
||||
that's no longer the case, you can now remove the `""` and take advantage of
|
||||
writing nice `yaml` files that just work.
|
||||
|
||||
```diff
|
||||
spec:
|
||||
steps:
|
||||
input:
|
||||
allowedHosts: ['github.com']
|
||||
- description: 'This is {{ parameters.name }}'
|
||||
+ description: This is ${{ parameters.name }}
|
||||
- repoUrl: '{{ parameters.repoUrl }}'
|
||||
+ repoUrl: ${{ parameters.repoUrl }}
|
||||
```yaml
|
||||
spec:
|
||||
steps:
|
||||
input:
|
||||
allowedHosts: ['github.com']
|
||||
# highlight-remove-next-line
|
||||
description: 'This is {{ parameters.name }}'
|
||||
# highlight-add-next-line
|
||||
description: This is ${{ parameters.name }}
|
||||
# highlight-remove-next-line
|
||||
repoUrl: '{{ parameters.repoUrl }}'
|
||||
# highlight-add-next-line
|
||||
repoUrl: ${{ parameters.repoUrl }}
|
||||
```
|
||||
|
||||
## No more `eq` or `not` helpers
|
||||
@@ -85,24 +97,26 @@ These helpers are no longer needed with the more expressive `api` that
|
||||
`nunjucks` provides. You can simply use the built-in `nunjucks` and `jinja2`
|
||||
style operators.
|
||||
|
||||
```diff
|
||||
spec:
|
||||
steps:
|
||||
input:
|
||||
- if: '{{ eq parameters.value "backstage" }}'
|
||||
+ if: ${{ parameters.value === "backstage" }}
|
||||
...
|
||||
```yaml
|
||||
spec:
|
||||
steps:
|
||||
input:
|
||||
# highlight-remove-next-line
|
||||
if: '{{ eq parameters.value "backstage" }}'
|
||||
# highlight-add-next-line
|
||||
if: ${{ parameters.value === "backstage" }}
|
||||
```
|
||||
|
||||
And then for the `not`
|
||||
|
||||
```diff
|
||||
spec:
|
||||
steps:
|
||||
input:
|
||||
- if: '{{ not parameters.value "backstage" }}'
|
||||
+ if: ${{ parameters.value !== "backstage" }}
|
||||
...
|
||||
```yaml
|
||||
spec:
|
||||
steps:
|
||||
input:
|
||||
# highlight-remove-next-line
|
||||
if: '{{ not parameters.value "backstage" }}'
|
||||
# highlight-add-next-line
|
||||
if: ${{ parameters.value !== "backstage" }}
|
||||
```
|
||||
|
||||
Much better right? ✨
|
||||
@@ -115,32 +129,36 @@ supporting the additional primitive values now rather than everything being a
|
||||
should all work as expected and keep the type that has been declared in the
|
||||
input schema.
|
||||
|
||||
```diff
|
||||
spec:
|
||||
parameters:
|
||||
test:
|
||||
type: number
|
||||
name: Test Number
|
||||
address:
|
||||
type: object
|
||||
required:
|
||||
- line1
|
||||
properties:
|
||||
line1:🙏
|
||||
type: string
|
||||
name: Line 1
|
||||
line2:
|
||||
type: string
|
||||
name: Line 2
|
||||
```yaml
|
||||
spec:
|
||||
parameters:
|
||||
test:
|
||||
type: number
|
||||
name: Test Number
|
||||
address:
|
||||
type: object
|
||||
required:
|
||||
- line1
|
||||
properties:
|
||||
line1:
|
||||
type: string
|
||||
name: Line 1
|
||||
line2:
|
||||
type: string
|
||||
name: Line 2
|
||||
|
||||
steps:
|
||||
- id: test step
|
||||
action: run:something
|
||||
input:
|
||||
- address: '{{ json parameters.address }}'
|
||||
+ address: ${{ parameters.address }}
|
||||
- test: '{{ parameters.test }}'
|
||||
+ test: ${{ parameters.test }} # this will now make sure that the type of test is a number 🙏
|
||||
steps:
|
||||
- id: test step
|
||||
action: run:something
|
||||
input:
|
||||
# highlight-remove-next-line
|
||||
address: '{{ json parameters.address }}'
|
||||
# highlight-add-next-line
|
||||
address: ${{ parameters.address }}
|
||||
# highlight-remove-next-line
|
||||
test: '{{ parameters.test }}'
|
||||
# highlight-add-next-line
|
||||
test: ${{ parameters.test }} # this will now make sure that the type of test is a number 🙏
|
||||
```
|
||||
|
||||
## `parseRepoUrl` is now a `filter`
|
||||
@@ -148,13 +166,14 @@ input schema.
|
||||
All calls to `parseRepoUrl` are now a `jinja2` `filter`, which means you'll need
|
||||
to update the syntax.
|
||||
|
||||
```diff
|
||||
spec:
|
||||
steps:
|
||||
input:
|
||||
- repoUrl: '{{ parseRepoUrl parameters.repoUrl }}'
|
||||
+ repoUrl: ${{ parameters.repoUrl | parseRepoUrl }}
|
||||
...
|
||||
```yaml
|
||||
spec:
|
||||
steps:
|
||||
input:
|
||||
# highlight-remove-next-line
|
||||
repoUrl: '{{ parseRepoUrl parameters.repoUrl }}'
|
||||
# highlight-add-next-line
|
||||
repoUrl: ${{ parameters.repoUrl | parseRepoUrl }}
|
||||
```
|
||||
|
||||
Now we have complex value support here too, expect that this `filter` will go
|
||||
@@ -167,39 +186,46 @@ away in future versions and the `RepoUrlPicker` will return an object so
|
||||
Previously, it was possible to provide links to the frontend using the named output `entityRef` and `remoteUrl`.
|
||||
These should be moved to `links` under the `output` object instead.
|
||||
|
||||
```diff
|
||||
output:
|
||||
- remoteUrl: {{ steps['publish'].output.remoteUrl }}
|
||||
- entityRef: {{ steps['register'].output.entityRef }}
|
||||
+ links:
|
||||
+ - title: Repository
|
||||
+ url: ${{ steps['publish'].output.remoteUrl }}
|
||||
+ - title: Open in catalog
|
||||
+ icon: catalog
|
||||
+ entityRef: ${{ steps['register'].output.entityRef }}
|
||||
|
||||
```yaml
|
||||
output:
|
||||
# highlight-remove-start
|
||||
remoteUrl: {{ steps['publish'].output.remoteUrl }}
|
||||
entityRef: {{ steps['register'].output.entityRef }}
|
||||
# highlight-remove-end
|
||||
# highlight-add-start
|
||||
links:
|
||||
- title: Repository
|
||||
url: ${{ steps['publish'].output.remoteUrl }}
|
||||
- title: Open in catalog
|
||||
icon: catalog
|
||||
entityRef: ${{ steps['register'].output.entityRef }}
|
||||
# highlight-add-end
|
||||
```
|
||||
|
||||
## Watch out for `dash-case`
|
||||
|
||||
The nunjucks compiler can run into issues if the `id` fields in your template steps use dash characters, since these IDs translate directly to JavaScript object properties when accessed as output. One possible migration path is to use `camelCase` for your action IDs.
|
||||
|
||||
```diff
|
||||
```yaml
|
||||
steps:
|
||||
- id: my-custom-action
|
||||
- ...
|
||||
-
|
||||
- id: publish-pull-request
|
||||
- input:
|
||||
- repoUrl: {{ steps.my-custom-action.output.repoUrl }} # Will not recognize 'my-custom-action' as a JS property since it contains dashes!
|
||||
# highlight-remove-start
|
||||
id: my-custom-action
|
||||
...
|
||||
|
||||
id: publish-pull-request
|
||||
input:
|
||||
repoUrl: {{ steps.my-custom-action.output.repoUrl }} # Will not recognize 'my-custom-action' as a JS property since it contains dashes!
|
||||
# highlight-remove-end
|
||||
|
||||
steps:
|
||||
+ id: myCustomAction
|
||||
+ ...
|
||||
+
|
||||
+ id: publishPullRequest
|
||||
+ input:
|
||||
+ repoUrl: ${{ steps.myCustomAction.output.repoUrl }}
|
||||
# highlight-add-start
|
||||
id: myCustomAction
|
||||
...
|
||||
|
||||
id: publishPullRequest
|
||||
input:
|
||||
repoUrl: ${{ steps.myCustomAction.output.repoUrl }}
|
||||
# highlight-add-end
|
||||
```
|
||||
|
||||
Alternatively, it's possible to keep the `dash-case` syntax and use brackets for property access as you would in JavaScript:
|
||||
|
||||
@@ -41,39 +41,42 @@ It's also worth calling out that if you do test this out, and find some issues o
|
||||
|
||||
The `ScaffolderPage` router has a completely different export for the `scaffolder/next` work, so you will want to change any import from the old `ScaffolderPage` to the new `NextScaffolderPage`
|
||||
|
||||
```diff
|
||||
- import { ScaffolderPage } from '@backstage/plugin-scaffolder';
|
||||
+ import { NextScaffolderPage } from '@backstage/plugin-scaffolder/alpha';
|
||||
|
||||
```tsx
|
||||
/* highlight-remove-next-line */
|
||||
import { ScaffolderPage } from '@backstage/plugin-scaffolder';
|
||||
/* highlight-add-next-line */
|
||||
import { NextScaffolderPage } from '@backstage/plugin-scaffolder/alpha';
|
||||
```
|
||||
|
||||
And this API should be the exact same as the previous Router, so you should be able to make a change like the following further down in this file:
|
||||
|
||||
```diff
|
||||
<Route
|
||||
path="/create"
|
||||
element={
|
||||
- <ScaffolderPage
|
||||
+ <NextScaffolderPage
|
||||
groups={[
|
||||
{
|
||||
title: 'Recommended',
|
||||
filter: entity =>
|
||||
entity?.metadata?.tags?.includes('recommended') ?? false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ScaffolderFieldExtensions>
|
||||
<LowerCaseValuePickerFieldExtension />
|
||||
... other extensions
|
||||
</ScaffolderFieldExtensions>
|
||||
<ScaffolderLayouts>
|
||||
<TwoColumnLayout />
|
||||
... other layouts
|
||||
</ScaffolderLayouts>
|
||||
</Route>
|
||||
```tsx
|
||||
<Route
|
||||
path="/create"
|
||||
element={
|
||||
{/* highlight-remove-next-line */}
|
||||
<ScaffolderPage
|
||||
{/* highlight-add-next-line */}
|
||||
<NextScaffolderPage
|
||||
groups={[
|
||||
{
|
||||
title: 'Recommended',
|
||||
filter: entity =>
|
||||
entity?.metadata?.tags?.includes('recommended') ?? false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ScaffolderFieldExtensions>
|
||||
<LowerCaseValuePickerFieldExtension />
|
||||
{/* ... other extensions */}
|
||||
</ScaffolderFieldExtensions>
|
||||
<ScaffolderLayouts>
|
||||
<TwoColumnLayout />
|
||||
{/* ... other layouts */}
|
||||
</ScaffolderLayouts>
|
||||
</Route>
|
||||
```
|
||||
|
||||
### Make the required changes to your `CustomFieldExtensions`
|
||||
@@ -95,13 +98,17 @@ export const EntityNamePickerFieldExtension = scaffolderPlugin.provide(
|
||||
|
||||
References for `createScaffolderFieldExtension` have an `/alpha` version of `createNextScaffolderFieldExtension`, which should be used instead.
|
||||
|
||||
```diff
|
||||
-import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder';
|
||||
+import { createNextScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
```ts
|
||||
/* highlight-remove-next-line */
|
||||
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder';
|
||||
/* highlight-add-next-line */
|
||||
import { createNextScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
export const EntityNamePickerFieldExtension = scaffolderPlugin.provide(
|
||||
- createScaffolderFieldExtension({
|
||||
+ createNextScaffolderFieldExtension({
|
||||
/* highlight-remove-next-line */
|
||||
createScaffolderFieldExtension({
|
||||
/* highlight-add-next-line */
|
||||
createNextScaffolderFieldExtension({
|
||||
component: EntityNamePicker,
|
||||
name: 'EntityNamePicker',
|
||||
validation: entityNamePickerValidation,
|
||||
@@ -113,7 +120,7 @@ Once you've done this you will find that you will have two squiggly lines under
|
||||
|
||||
Let's take the following code for the `EntityNamePicker` component:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
export const EntityNamePicker = (
|
||||
props: FieldExtensionComponentProps<string, EntityNamePickerProps>,
|
||||
) => {
|
||||
@@ -127,19 +134,23 @@ export const EntityNamePicker = (
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
...
|
||||
}
|
||||
// ..
|
||||
};
|
||||
```
|
||||
|
||||
There's another `/alpha` export that you need to replace `FieldExtensionComponentProps` with which is the `NextFieldExtensionComponentProps`.
|
||||
|
||||
```diff
|
||||
- import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
|
||||
+ import { NextFieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
```tsx
|
||||
/* highlight-remove-next-line */
|
||||
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
|
||||
/* highlight-add-next-line */
|
||||
import { NextFieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
|
||||
export const EntityNamePicker = (
|
||||
- props: FieldExtensionComponentProps<string, EntityNamePickerProps>,
|
||||
+ props: NextFieldExtensionComponentProps<string, EntityNamePickerProps>,
|
||||
/* highlight-remove-next-line */
|
||||
props: FieldExtensionComponentProps<string, EntityNamePickerProps>,
|
||||
/* highlight-add-next-line */
|
||||
props: NextFieldExtensionComponentProps<string, EntityNamePickerProps>,
|
||||
) => {
|
||||
const {
|
||||
onChange,
|
||||
@@ -147,13 +158,15 @@ export const EntityNamePicker = (
|
||||
schema: { title = 'Name', description = 'Unique name of the component' },
|
||||
rawErrors,
|
||||
formData,
|
||||
- uiSchema: { 'ui:autofocus': autoFocus },
|
||||
+ uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
/* highlight-remove-next-line */
|
||||
uiSchema: { 'ui:autofocus': autoFocus },
|
||||
/* highlight-add-next-line */
|
||||
uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
...
|
||||
}
|
||||
// ..
|
||||
};
|
||||
```
|
||||
|
||||
You'll notice that there's an additional change here, which is that we're now defaulting the `uiSchema` to an empty object. This is because the `uiSchema` is now optional, and if you don't provide it, it will be `undefined` instead of an empty object. There's more around this in the [breaking changes](#breaking-changes) section.
|
||||
@@ -182,8 +195,10 @@ You will need to change the import for `FieldValidation` to point at the new `re
|
||||
|
||||
> Note: you will probably need to install this dependency too, by using `yarn add @rjsf/utils` in the package where you define these validation functions, this could also be in the `packages/app` folder, so you can install it there if needed.
|
||||
|
||||
```diff
|
||||
```ts
|
||||
/* highlight-remove-next-line */
|
||||
- import { FieldValidation } from '@rjsf/core';
|
||||
/* highlight-add-next-line */
|
||||
+ import { FieldValidation } from '@rjsf/utils;
|
||||
import { KubernetesValidatorFunctions } from '@backstage/catalog-model';
|
||||
|
||||
@@ -201,34 +216,40 @@ Once we fully release the code that is in the `/alpha` exports right now onto th
|
||||
|
||||
Later releases of `react-jsonschema-form` have made the `uiSchema` optional, and if you don't provide it, it will be `undefined` instead of an empty object. This means that you will need to make sure that you're defaulting the `uiSchema` to an empty object if you're using it in your code.
|
||||
|
||||
```diff
|
||||
const {
|
||||
onChange,
|
||||
required,
|
||||
schema: { title = 'Name', description = 'Unique name of the component' },
|
||||
rawErrors,
|
||||
formData,
|
||||
- uiSchema: { 'ui:autofocus': autoFocus },
|
||||
+ uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
```tsx
|
||||
const {
|
||||
onChange,
|
||||
required,
|
||||
schema: { title = 'Name', description = 'Unique name of the component' },
|
||||
rawErrors,
|
||||
formData,
|
||||
/* highlight-remove-next-line */
|
||||
uiSchema: { 'ui:autofocus': autoFocus },
|
||||
/* highlight-add-next-line */
|
||||
uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
// ..
|
||||
```
|
||||
|
||||
### `formData` can also be `undefined`
|
||||
|
||||
If you were using the `formData` and assuming that it was set to an empty object when building `Field Extensions` that return objects, then this will be `undefined` now due to a change in the `react-jsonschema-form` library.
|
||||
|
||||
```diff
|
||||
const {
|
||||
onChange,
|
||||
required,
|
||||
schema: { title = 'Name', description = 'Unique name of the component' },
|
||||
rawErrors,
|
||||
- formData,
|
||||
+ formData = {}, // or maybe some other default value that you would prefer
|
||||
uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
```tsx
|
||||
const {
|
||||
onChange,
|
||||
required,
|
||||
schema: { title = 'Name', description = 'Unique name of the component' },
|
||||
rawErrors,
|
||||
/* highlight-remove-next-line */
|
||||
formData,
|
||||
/* highlight-add-next-line */
|
||||
formData = {}, // or maybe some other default value that you would prefer
|
||||
uiSchema: { 'ui:autofocus': autoFocus } = {},
|
||||
idSchema,
|
||||
placeholder,
|
||||
} = props;
|
||||
// ..
|
||||
```
|
||||
|
||||
@@ -31,18 +31,16 @@ Once the package has been installed, you need to import the plugin in your app.
|
||||
In `packages/app/src/App.tsx`, import `TechDocsPage` and add the following to
|
||||
`FlatRoutes`:
|
||||
|
||||
```tsx
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
import {
|
||||
DefaultTechDocsHome,
|
||||
TechDocsIndexPage,
|
||||
TechDocsReaderPage,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
|
||||
// ...
|
||||
|
||||
const AppRoutes = () => {
|
||||
<FlatRoutes>
|
||||
// ... other plugin routes
|
||||
{/* ... other plugin routes */}
|
||||
<Route path="/docs" element={<TechDocsIndexPage />}>
|
||||
<DefaultTechDocsHome />
|
||||
</Route>
|
||||
@@ -58,20 +56,20 @@ It would be nice to decorate your pages with something else... Having a link tha
|
||||
|
||||
With the [TechDocs Addon framework](https://backstage.io/docs/features/techdocs/addons#installing-and-using-addons), you can render React components in documentation pages and these Addons can be provided by any Backstage plugin. The framework is exported by the [@backstage/plugin-techdocs-react](https://www.npmjs.com/package/@backstage/plugin-techdocs-react) package and there is a `<ReportIssue />` Addon in the [@backstage/plugin-techdocs-module-addons-contrib](https://www.npmjs.com/package/@backstage/plugin-techdocs-module-addons-contrib) package for you to use once you have these two dependencies installed:
|
||||
|
||||
```diff
|
||||
```tsx
|
||||
import {
|
||||
DefaultTechDocsHome,
|
||||
TechDocsIndexPage,
|
||||
TechDocsReaderPage,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
+ import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
|
||||
+ import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
|
||||
|
||||
// ...
|
||||
/* highlight-add-start */
|
||||
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
|
||||
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
|
||||
/* highlight-add-end */
|
||||
|
||||
const AppRoutes = () => {
|
||||
<FlatRoutes>
|
||||
// ... other plugin routes
|
||||
{/* ... other plugin routes */}
|
||||
<Route path="/docs" element={<TechDocsIndexPage />}>
|
||||
<DefaultTechDocsHome />
|
||||
</Route>
|
||||
@@ -79,9 +77,11 @@ const AppRoutes = () => {
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={<TechDocsReaderPage />}
|
||||
>
|
||||
+ <TechDocsAddons>
|
||||
+ <ReportIssue />
|
||||
+ </TechDocsAddons>
|
||||
{/* highlight-add-start */}
|
||||
<TechDocsAddons>
|
||||
<ReportIssue />
|
||||
</TechDocsAddons>
|
||||
{/* highlight-add-end */}
|
||||
</Route>
|
||||
</FlatRoutes>;
|
||||
};
|
||||
|
||||
@@ -172,7 +172,7 @@ When creating a custom theme you would be applying different values to
|
||||
component's css rules that use the theme object. For example, a Backstage
|
||||
component's styles might look like this:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
const useStyles = makeStyles<BackstageTheme>(
|
||||
theme => ({
|
||||
header: {
|
||||
@@ -263,11 +263,9 @@ You can also use another web image format such as PNG by importing it. To do
|
||||
this, place your new image into a new subdirectory such as
|
||||
`src/components/Root/logo/my-company-logo.png`, and then add this code:
|
||||
|
||||
```jsx
|
||||
```tsx
|
||||
import MyCustomLogoFull from './logo/my-company-logo.png';
|
||||
|
||||
//...
|
||||
|
||||
const LogoFull = () => {
|
||||
return <img src={MyCustomLogoFull} />;
|
||||
};
|
||||
@@ -294,8 +292,7 @@ In your front-end application, locate the `src` folder. We suggest creating the
|
||||
|
||||
> Another example [here](https://github.com/backstage/backstage/blob/master/plugins/azure-devops/src/components/AzurePipelinesIcon/AzurePipelinesIcon.tsx), if you want to ensure proper behavior in light and dark themes.
|
||||
|
||||
```tsx
|
||||
// customIcons.tsx
|
||||
```tsx title="customIcons.tsx"
|
||||
import { SvgIcon, SvgIconProps } from '@material-ui/core';
|
||||
|
||||
import React from 'react';
|
||||
@@ -317,29 +314,28 @@ export const ExampleIcon = (props: SvgIconProps) => (
|
||||
|
||||
Supply your custom icon in `packages/app/src/App.tsx`
|
||||
|
||||
```diff
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { ExampleIcon } from './assets/customIcons'
|
||||
|
||||
+ import { ExampleIcon } from './assets/customIcons'
|
||||
|
||||
[...]
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
components: {
|
||||
[...]
|
||||
{/* ... */}
|
||||
},
|
||||
themes: [
|
||||
[...]
|
||||
{/* ... */}
|
||||
],
|
||||
+ icons: {
|
||||
+ github: ExampleIcon,
|
||||
+ },
|
||||
/* highlight-add-start */
|
||||
icons: {
|
||||
github: ExampleIcon,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
bindRoutes({ bind }) {
|
||||
[...]
|
||||
{/* ... */}
|
||||
}
|
||||
})
|
||||
|
||||
[...]
|
||||
```
|
||||
|
||||
### Adding Icons
|
||||
@@ -350,16 +346,18 @@ You can add more icons, if the [default icons](https://github.com/backstage/back
|
||||
2. Then you want to import your icon, add this to the rest of your imports: `import AlarmIcon from '@material-ui/icons/Alarm';`
|
||||
3. Next you want to add the icon like this to your `createApp`:
|
||||
|
||||
```diff
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
+ icons: {
|
||||
+ alert: AlarmIcon,
|
||||
+ },
|
||||
themes: ...,
|
||||
components: ...,
|
||||
});
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
/* highlight-add-start */
|
||||
icons: {
|
||||
alert: AlarmIcon,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
themes: ...,
|
||||
components: ...,
|
||||
});
|
||||
```
|
||||
|
||||
4. Now we can reference `alert` for our icon in our entity links like this:
|
||||
@@ -404,7 +402,7 @@ For this example we'll show you how you can expand the sidebar with a sub-menu:
|
||||
1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives
|
||||
2. Then we want to add the following imports for the icons:
|
||||
|
||||
```ts
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
import ApiIcon from '@material-ui/icons/Extension';
|
||||
import ComponentIcon from '@material-ui/icons/Memory';
|
||||
import DomainIcon from '@material-ui/icons/Apartment';
|
||||
@@ -415,27 +413,29 @@ For this example we'll show you how you can expand the sidebar with a sub-menu:
|
||||
|
||||
3. Then update the `@backstage/core-components` import like this:
|
||||
|
||||
```diff
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
useSidebarOpenState,
|
||||
Link,
|
||||
+ GroupIcon,
|
||||
+ SidebarSubmenu,
|
||||
+ SidebarSubmenuItem,
|
||||
} from '@backstage/core-components';
|
||||
```tsx
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
useSidebarOpenState,
|
||||
Link,
|
||||
/* highlight-add-start */
|
||||
GroupIcon,
|
||||
SidebarSubmenu,
|
||||
SidebarSubmenuItem,
|
||||
/* highlight-add-end */
|
||||
} from '@backstage/core-components';
|
||||
```
|
||||
|
||||
4. Finally replace `<SidebarItem icon={HomeIcon} to="catalog" text="Home" />` with this:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem
|
||||
|
||||
@@ -74,22 +74,26 @@ Use your favorite editor to open `app-config.yaml` and add your PostgreSQL
|
||||
configuration. in the root directory of your Backstage app using the credentials
|
||||
from the previous steps.
|
||||
|
||||
```diff
|
||||
```yaml title="app-config.yaml"
|
||||
backend:
|
||||
database:
|
||||
- client: better-sqlite3
|
||||
- connection: ':memory:'
|
||||
+ # config options: https://node-postgres.com/apis/client
|
||||
+ client: pg
|
||||
+ connection:
|
||||
+ host: ${POSTGRES_HOST}
|
||||
+ port: ${POSTGRES_PORT}
|
||||
+ user: ${POSTGRES_USER}
|
||||
+ password: ${POSTGRES_PASSWORD}
|
||||
+ # https://node-postgres.com/features/ssl
|
||||
+ #ssl: require # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require)
|
||||
+ #ca: # if you have a CA file and want to verify it you can uncomment this section
|
||||
+ #$file: <file-path>/ca/server.crt
|
||||
# highlight-remove-start
|
||||
client: better-sqlite3
|
||||
connection: ':memory:'
|
||||
# highlight-remove-end
|
||||
# highlight-add-start
|
||||
# config options: https://node-postgres.com/apis/client
|
||||
client: pg
|
||||
connection:
|
||||
host: ${POSTGRES_HOST}
|
||||
port: ${POSTGRES_PORT}
|
||||
user: ${POSTGRES_USER}
|
||||
password: ${POSTGRES_PASSWORD}
|
||||
# https://node-postgres.com/features/ssl
|
||||
# ssl: require # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require)
|
||||
#ca: # if you have a CA file and want to verify it you can uncomment this section
|
||||
#$file: <file-path>/ca/server.crt
|
||||
# highlight-add-end
|
||||
```
|
||||
|
||||
You'll use the connection details from the previous step. You can either set the
|
||||
@@ -142,7 +146,7 @@ Take note of the `Client ID` and the `Client Secret`. Open `app-config.yaml`,
|
||||
and add your `clientId` and `clientSecret` to this file. It should end up
|
||||
looking like this:
|
||||
|
||||
```yaml
|
||||
```yaml title="app-config.yaml"
|
||||
auth:
|
||||
# see https://backstage.io/docs/auth/ to learn about auth providers
|
||||
environment: development
|
||||
@@ -161,14 +165,14 @@ change the sign-in page, this you actually need to add in the source code.
|
||||
|
||||
Open `packages/app/src/App.tsx` and below the last `import` line, add:
|
||||
|
||||
```typescript
|
||||
```typescript title="packages/app/src/App.tsx"
|
||||
import { githubAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { SignInPage } from '@backstage/core-components';
|
||||
```
|
||||
|
||||
Search for `const app = createApp({` in this file, and below `apis,` add:
|
||||
|
||||
```typescript
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
components: {
|
||||
SignInPage: props => (
|
||||
<SignInPage
|
||||
@@ -221,7 +225,7 @@ This file should also be excluded in `.gitignore`, to avoid accidental committin
|
||||
|
||||
In your `app-config.local.yaml` go ahead and add the following:
|
||||
|
||||
```yaml
|
||||
```yaml title="app-config.local.yaml"
|
||||
integrations:
|
||||
github:
|
||||
- host: github.com
|
||||
@@ -232,7 +236,7 @@ That's settled. This information will be leveraged by other plugins.
|
||||
|
||||
If you're looking for a more production way to manage this secret, then you can do the following with the token being stored in an environment variable called `GITHUB_TOKEN`.
|
||||
|
||||
```yaml
|
||||
```yaml title="app-config.local.yaml"
|
||||
integrations:
|
||||
github:
|
||||
- host: github.com
|
||||
|
||||
@@ -33,20 +33,22 @@ to an entity in the software catalog.
|
||||
|
||||
2. Add the `EntityCircleCIContent` extension to the entity pages in the app:
|
||||
|
||||
```diff
|
||||
// packages/app/src/components/catalog/EntityPage.tsx
|
||||
+import {
|
||||
+ EntityCircleCIContent,
|
||||
+ isCircleCIAvailable,
|
||||
+} from '@backstage/plugin-circleci';
|
||||
```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
EntityCircleCIContent,
|
||||
isCircleCIAvailable,
|
||||
} from '@backstage/plugin-circleci';
|
||||
/* highlight-add-end */
|
||||
|
||||
...
|
||||
const cicdContent = (
|
||||
<EntitySwitch>
|
||||
...
|
||||
+ <EntitySwitch.Case if={isCircleCIAvailable}>
|
||||
+ <EntityCircleCIContent />
|
||||
+ </EntitySwitch.Case>;
|
||||
{/* ... */}
|
||||
{/* highlight-add-next-line */}
|
||||
<EntitySwitch.Case if={isCircleCIAvailable}>
|
||||
<EntityCircleCIContent />
|
||||
</EntitySwitch.Case>
|
||||
;{/* highlight-add-end */}
|
||||
</EntitySwitch>
|
||||
);
|
||||
```
|
||||
@@ -62,8 +64,7 @@ to an entity in the software catalog.
|
||||
Plugins that collect data off of external services may require the use of a proxy service.
|
||||
This plugin accesses the CircleCI REST API, and thus requires a proxy definition.
|
||||
|
||||
```yaml
|
||||
// app-config.yaml
|
||||
```yaml title="app-config.yaml"
|
||||
proxy:
|
||||
'/circleci/api':
|
||||
target: https://circleci.com/api/v1.1
|
||||
@@ -82,7 +83,7 @@ adding new `SidebarItem` elements.
|
||||
For example, if you install the `api-docs` plugin, a matching `SidebarItem`
|
||||
could be something like this:
|
||||
|
||||
```tsx
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
// Import icon from MUI
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
|
||||
@@ -95,7 +96,7 @@ are sized according to the Material UI's
|
||||
[SvgIcon](https://material-ui.com/api/svg-icon/) default of 24x24px, and set the
|
||||
extension to `.icon.svg`. For example:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
import InternalToolIcon from './internal-tool.icon.svg';
|
||||
```
|
||||
|
||||
@@ -104,7 +105,7 @@ customizing the experience you can group `SidebarItems` in a `SidebarGroup`
|
||||
(Example 1) or create a `SidebarGroup` with a link (Example 2). All
|
||||
`SidebarGroup`s are displayed in the bottom navigation with an icon.
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
// Example 1
|
||||
<SidebarGroup icon={<MenuIcon />} label="Menu">
|
||||
...
|
||||
@@ -113,7 +114,7 @@ customizing the experience you can group `SidebarItems` in a `SidebarGroup`
|
||||
<SidebarGroup />
|
||||
```
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
// Example 2
|
||||
<SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
|
||||
...
|
||||
|
||||
@@ -52,30 +52,35 @@ If you don't have a homepage already, most likely you have a redirect setup to u
|
||||
|
||||
Inside your `packages/app/src/App.tsx`, look for
|
||||
|
||||
```tsx
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Navigate key="/" to="catalog" />
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
Let's replace the `<Navigate>` line and use the new component we created in the previous step as the new homepage.
|
||||
|
||||
```diff
|
||||
// File: packages/app/src/App.tsx
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-start */
|
||||
import { HomepageCompositionRoot } from '@backstage/plugin-home';
|
||||
import { HomePage } from './components/home/HomePage';
|
||||
/* highlight-add-end */
|
||||
|
||||
+ import { HomepageCompositionRoot } from '@backstage/plugin-home';
|
||||
+ import { HomePage } from './components/home/HomePage';
|
||||
|
||||
// ...
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
- <Navigate key="/" to="catalog" />
|
||||
+ <Route path="/" element={<HomepageCompositionRoot />}>
|
||||
+ <HomePage />
|
||||
+ </Route>
|
||||
// ...
|
||||
|
||||
)
|
||||
{/* highlight-remove-next-line */}
|
||||
<Navigate key="/" to="catalog" />
|
||||
{/* highlight-add-start */}
|
||||
<Route path="/" element={<HomepageCompositionRoot />}>
|
||||
<HomePage />
|
||||
</Route>
|
||||
{/* highlight-add-end */}
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
#### 4. Update sidebar items
|
||||
@@ -90,26 +95,34 @@ The code for the Backstage sidebar is most likely inside your [`packages/app/src
|
||||
|
||||
Let's make the following changes
|
||||
|
||||
```diff
|
||||
// Other imports
|
||||
+ import CategoryIcon from '@material-ui/icons/Category';
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import CategoryIcon from '@material-ui/icons/Category';
|
||||
|
||||
export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarLogo />
|
||||
# ...
|
||||
{/* ... */}
|
||||
<SidebarGroup label="Menu" icon={<MenuIcon />}>
|
||||
{/* Global nav, not org-specific */}
|
||||
- <SidebarItem icon={HomeIcon} to="catalog" text="Home" />
|
||||
+ <SidebarItem icon={HomeIcon} to="/" text="Home" />
|
||||
+ <SidebarItem icon={CategoryIcon} to="catalog" text="Catalog" />
|
||||
{/* highlight-remove-next-line */}
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
|
||||
{/* highlight-add-start */}
|
||||
<SidebarItem icon={HomeIcon} to="/" text="Home" />
|
||||
<SidebarItem icon={CategoryIcon} to="catalog" text="Catalog" />
|
||||
{/* highlight-add-end */}
|
||||
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
|
||||
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
|
||||
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
|
||||
{/* End global nav */}
|
||||
<SidebarDivider />
|
||||
{/* ... */}
|
||||
</SidebarGroup>
|
||||
</Sidebar>
|
||||
</SidebarPage>
|
||||
);
|
||||
```
|
||||
|
||||
That's it! You should now have _(although slightly boring)_ a homepage!
|
||||
|
||||
@@ -32,8 +32,7 @@ Setup [Azure integration](locations.md) with `host` and `token`. Host must be `d
|
||||
|
||||
At your configuration, you add one or more provider configs:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
```yaml title="app-config.yaml"
|
||||
catalog:
|
||||
providers:
|
||||
azureDevOps:
|
||||
@@ -94,39 +93,49 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-azure
|
||||
|
||||
Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`:
|
||||
|
||||
```diff
|
||||
/* packages/backend/src/plugins/catalog.ts */
|
||||
+import { AzureDevOpsEntityProvider } from '@backstage/plugin-catalog-backend-module-azure';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { AzureDevOpsEntityProvider } from '@backstage/plugin-catalog-backend-module-azure';
|
||||
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/** ... other processors and/or providers ... */
|
||||
+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: { minutes: 30 },
|
||||
+ timeout: { minutes: 3 },
|
||||
+ }),
|
||||
+ // optional: alternatively, use schedule
|
||||
+ scheduler: env.scheduler,
|
||||
+ }),
|
||||
+);
|
||||
/* highlight-add-start */
|
||||
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: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
// optional: alternatively, use schedule
|
||||
scheduler: env.scheduler,
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
## Alternative Processor
|
||||
|
||||
As an alternative to the entity provider `AzureDevOpsEntityProvider`, you can still use the `AzureDevopsDiscoveryProcessor`.
|
||||
|
||||
```diff
|
||||
// In packages/backend/src/plugins/catalog.ts
|
||||
+import { AzureDevOpsDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-azure';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { AzureDevOpsDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-azure';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
+ builder.addProcessor(AzureDevOpsDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }));
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-next-line */
|
||||
builder.addProcessor(
|
||||
AzureDevOpsDiscoveryProcessor.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
}),
|
||||
);
|
||||
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -21,7 +21,7 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-msgraph
|
||||
|
||||
Next add the basic configuration to `app-config.yaml`
|
||||
|
||||
```yaml
|
||||
```yaml title="app-config.yaml"
|
||||
catalog:
|
||||
providers:
|
||||
microsoftGraphOrg:
|
||||
@@ -39,25 +39,30 @@ catalog:
|
||||
Finally, register the plugin in `catalog.ts`.
|
||||
For large organizations, this plugin can take a long time, so be careful setting low frequency / timeouts.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
+import { MicrosoftGraphOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-msgraph';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { MicrosoftGraphOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-msgraph';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
|
||||
+ builder.addEntityProvider(
|
||||
+ MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { hours: 1 },
|
||||
+ timeout: { minutes: 50 },
|
||||
+ initialDelay: { seconds: 15}
|
||||
+ }),
|
||||
+ }),
|
||||
+ );
|
||||
/* highlight-add-start */
|
||||
builder.addEntityProvider(
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { hours: 1 },
|
||||
timeout: { minutes: 50 },
|
||||
initialDelay: { seconds: 15 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
## Authenticating with Microsoft Graph
|
||||
@@ -71,7 +76,7 @@ If you can't do this, you'll have to create an App Registration.
|
||||
|
||||
### App Registration
|
||||
|
||||
If none of the other authentication methods work, you can create an app registration in the azure portal.
|
||||
If none of the other authentication methods work, you can create an app registration in the azure portal.
|
||||
By default the graph plugin requires the following Application permissions (not Delegated) for Microsoft Graph:
|
||||
|
||||
- `GroupMember.Read.All`
|
||||
@@ -150,15 +155,17 @@ Entities can also be excluded from backstage by returning `undefined`.
|
||||
|
||||
These Transformers are be registered when configuring `MicrosoftGraphOrgEntityProvider`
|
||||
|
||||
```diff
|
||||
builder.addEntityProvider(
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
// ...
|
||||
+ groupTransformer: myGroupTransformer,
|
||||
+ userTransformer: myUserTransformer,
|
||||
+ organizationTransformer: myOrganizationTransformer,
|
||||
}),
|
||||
);
|
||||
```ts
|
||||
builder.addEntityProvider(
|
||||
MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
// ...
|
||||
/* highlight-add-start */
|
||||
groupTransformer: myGroupTransformer,
|
||||
userTransformer: myUserTransformer,
|
||||
organizationTransformer: myOrganizationTransformer,
|
||||
/* highlight-add-end */
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
When using custom transformers, you may want to customize the data returned.
|
||||
@@ -243,7 +250,7 @@ Try importing a smaller set of data (e.g. `filter: displayName eq 'John Smith'`)
|
||||
|
||||
See [Troubleshooting Azure Identity Authentication Issues](https://aka.ms/azsdk/js/identity/troubleshoot)
|
||||
|
||||
### Error while reading users from Microsoft Graph: Authorization_RequestDenied - Insufficient privileges to complete the operation.
|
||||
### Error while reading users from Microsoft Graph: Authorization_RequestDenied - Insufficient privileges to complete the operation
|
||||
|
||||
- Make sure you've granted all the required permissions to your application registration or managed identity
|
||||
- Make sure the permissions are `Application` permissions rather than `Delegated`
|
||||
|
||||
@@ -28,41 +28,46 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-bitbuck
|
||||
|
||||
And then add the entity provider to your catalog builder:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
+ import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
+ builder.addEntityProvider(
|
||||
+ BitbucketCloudEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ scheduler: env.scheduler,
|
||||
+ }),
|
||||
+ );
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-start */
|
||||
builder.addEntityProvider(
|
||||
BitbucketCloudEntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
scheduler: env.scheduler,
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// [...]
|
||||
}
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively to the config-based schedule, you can use
|
||||
|
||||
```diff
|
||||
- scheduler: env.scheduler,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { minutes: 30 },
|
||||
+ timeout: { minutes: 3 },
|
||||
+ }),
|
||||
```ts
|
||||
/* highlight-remove-next-line */
|
||||
scheduler: env.scheduler,
|
||||
/* highlight-add-start */
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
### Installation with Events Support
|
||||
|
||||
Please follow the installation instructions at
|
||||
|
||||
- https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md
|
||||
- https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-cloud/README.md
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md>
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-cloud/README.md>
|
||||
|
||||
Additionally, you need to decide how you want to receive events from external sources like
|
||||
|
||||
@@ -71,32 +76,37 @@ Additionally, you need to decide how you want to receive events from external so
|
||||
|
||||
Set up your provider
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/catalogEventBasedProviders.ts
|
||||
+import { CatalogClient } from '@backstage/catalog-client';
|
||||
+import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-node';
|
||||
import { EventSubscriber } from '@backstage/plugin-events-node';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```ts title="packages/backend/src/plugins/catalogEventBasedProviders.ts"
|
||||
/* highlight-add-start */
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud';
|
||||
/* highlight-add-end */
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-node';
|
||||
import { EventSubscriber } from '@backstage/plugin-events-node';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createCatalogEventBasedProviders(
|
||||
- _: PluginEnvironment,
|
||||
+ env: PluginEnvironment,
|
||||
): Promise<Array<EntityProvider & EventSubscriber>> {
|
||||
const providers: Array<
|
||||
(EntityProvider & EventSubscriber) | Array<EntityProvider & EventSubscriber>
|
||||
> = [];
|
||||
- // add your event-based entity providers here
|
||||
+ providers.push(
|
||||
+ BitbucketCloudEntityProvider.fromConfig(env.config, {
|
||||
+ catalogApi: new CatalogClient({ discoveryApi: env.discovery }),
|
||||
+ logger: env.logger,
|
||||
+ scheduler: env.scheduler,
|
||||
+ tokenManager: env.tokenManager,
|
||||
+ }),
|
||||
+ );
|
||||
return providers.flat();
|
||||
}
|
||||
export default async function createCatalogEventBasedProviders(
|
||||
/* highlight-remove-next-line */
|
||||
_: PluginEnvironment,
|
||||
/* highlight-add-next-line */
|
||||
env: PluginEnvironment,
|
||||
): Promise<Array<EntityProvider & EventSubscriber>> {
|
||||
const providers: Array<
|
||||
(EntityProvider & EventSubscriber) | Array<EntityProvider & EventSubscriber>
|
||||
> = [];
|
||||
// add your event-based entity providers here
|
||||
/* highlight-add-start */
|
||||
providers.push(
|
||||
BitbucketCloudEntityProvider.fromConfig(env.config, {
|
||||
catalogApi: new CatalogClient({ discoveryApi: env.discovery }),
|
||||
logger: env.logger,
|
||||
scheduler: env.scheduler,
|
||||
tokenManager: env.tokenManager,
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
return providers.flat();
|
||||
}
|
||||
```
|
||||
|
||||
**Attention:**
|
||||
@@ -111,9 +121,7 @@ Very likely a `username` and `appPassword` will be required
|
||||
|
||||
Additionally, you need to configure your entity provider instance(s):
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
|
||||
```yaml title="app-config.yaml"
|
||||
catalog:
|
||||
providers:
|
||||
bitbucketCloud:
|
||||
|
||||
@@ -26,29 +26,31 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-bitbuck
|
||||
|
||||
And then add the entity provider to your catalog builder:
|
||||
|
||||
```diff
|
||||
// In packages/backend/src/plugins/catalog.ts
|
||||
+ import { BitbucketServerEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-server';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { BitbucketServerEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-server';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
+ 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,
|
||||
+ }),
|
||||
+ );
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-start */
|
||||
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,
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// [...]
|
||||
}
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -57,9 +59,7 @@ To use the entity provider, you'll need a [Bitbucket Server integration set up](
|
||||
|
||||
Additionally, you need to configure your entity provider instance(s):
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
|
||||
```yaml title="app-config.yaml"
|
||||
catalog:
|
||||
providers:
|
||||
bitbucketServer:
|
||||
|
||||
@@ -28,37 +28,39 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-github
|
||||
|
||||
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';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
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, {
|
||||
+ 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,
|
||||
+ }),
|
||||
+ );
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-start */
|
||||
builder.addEntityProvider(
|
||||
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,
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// [...]
|
||||
}
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
## Installation with Events Support
|
||||
|
||||
Please follow the installation instructions at
|
||||
|
||||
- https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md
|
||||
- https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md>
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md>
|
||||
|
||||
Additionally, you need to decide how you want to receive events from external sources like
|
||||
|
||||
@@ -67,34 +69,38 @@ Additionally, you need to decide how you want to receive events from external so
|
||||
|
||||
Set up your provider
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/catalogEventBasedProviders.ts
|
||||
+import { GithubEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-node';
|
||||
import { EventSubscriber } from '@backstage/plugin-events-node';
|
||||
import { PluginEnvironment } from '../types';
|
||||
export default async function createCatalogEventBasedProviders(
|
||||
- _: PluginEnvironment,
|
||||
+ env: PluginEnvironment,
|
||||
): Promise<Array<EntityProvider & EventSubscriber>> {
|
||||
const providers: Array<
|
||||
(EntityProvider & EventSubscriber) | Array<EntityProvider & EventSubscriber>
|
||||
> = [];
|
||||
- // add your event-based entity providers here
|
||||
+ providers.push(
|
||||
+ 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,
|
||||
+ }),
|
||||
+ );
|
||||
return providers.flat();
|
||||
}
|
||||
```ts title="packages/backend/src/plugins/catalogEventBasedProviders.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { GithubEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-node';
|
||||
import { EventSubscriber } from '@backstage/plugin-events-node';
|
||||
import { PluginEnvironment } from '../types';
|
||||
export default async function createCatalogEventBasedProviders(
|
||||
/* highlight-remove-next-line */
|
||||
_: PluginEnvironment,
|
||||
/* highlight-add-next-line */
|
||||
env: PluginEnvironment,
|
||||
): Promise<Array<EntityProvider & EventSubscriber>> {
|
||||
const providers: Array<
|
||||
(EntityProvider & EventSubscriber) | Array<EntityProvider & EventSubscriber>
|
||||
> = [];
|
||||
// add your event-based entity providers here
|
||||
/* highlight-add-start */
|
||||
providers.push(
|
||||
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,
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
return providers.flat();
|
||||
}
|
||||
```
|
||||
|
||||
You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks). The webhook will need to be configured to forward `push` events.
|
||||
@@ -255,34 +261,40 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-github
|
||||
|
||||
And then add the processors to your catalog builder:
|
||||
|
||||
```diff
|
||||
// In packages/backend/src/plugins/catalog.ts
|
||||
+import {
|
||||
+ GithubDiscoveryProcessor,
|
||||
+ GithubOrgReaderProcessor,
|
||||
+} from '@backstage/plugin-catalog-backend-module-github';
|
||||
+import {
|
||||
+ ScmIntegrations,
|
||||
+ DefaultGithubCredentialsProvider
|
||||
+} from '@backstage/integration';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
GithubDiscoveryProcessor,
|
||||
GithubOrgReaderProcessor,
|
||||
} from '@backstage/plugin-catalog-backend-module-github';
|
||||
import {
|
||||
ScmIntegrations,
|
||||
DefaultGithubCredentialsProvider,
|
||||
} from '@backstage/integration';
|
||||
/* highlight-add-end */
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
+ const integrations = ScmIntegrations.fromConfig(env.config);
|
||||
+ const githubCredentialsProvider =
|
||||
+ DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
+ builder.addProcessor(
|
||||
+ GithubDiscoveryProcessor.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ githubCredentialsProvider,
|
||||
+ }),
|
||||
+ GithubOrgReaderProcessor.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ githubCredentialsProvider,
|
||||
+ }),
|
||||
+ );
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-start */
|
||||
const integrations = ScmIntegrations.fromConfig(env.config);
|
||||
const githubCredentialsProvider =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
builder.addProcessor(
|
||||
GithubDiscoveryProcessor.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
githubCredentialsProvider,
|
||||
}),
|
||||
GithubOrgReaderProcessor.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
githubCredentialsProvider,
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -36,36 +36,41 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-github
|
||||
Update the catalog plugin initialization in your backend to add the provider and
|
||||
schedule it:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
+import { GithubOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { GithubOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
|
||||
+ // The org URL below needs to match a configured integrations.github entry
|
||||
+ // specified in your app-config.
|
||||
+ builder.addEntityProvider(
|
||||
+ GithubOrgEntityProvider.fromConfig(env.config, {
|
||||
+ id: 'production',
|
||||
+ orgUrl: 'https://github.com/backstage',
|
||||
+ logger: env.logger,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { minutes: 60 },
|
||||
+ timeout: { minutes: 15 },
|
||||
+ }),
|
||||
+ }),
|
||||
+ );
|
||||
/* highlight-add-start */
|
||||
// The org URL below needs to match a configured integrations.github entry
|
||||
// specified in your app-config.
|
||||
builder.addEntityProvider(
|
||||
GithubOrgEntityProvider.fromConfig(env.config, {
|
||||
id: 'production',
|
||||
orgUrl: 'https://github.com/backstage',
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 60 },
|
||||
timeout: { minutes: 15 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
## Installation with Events Support
|
||||
|
||||
Please follow the installation instructions at
|
||||
|
||||
- https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md
|
||||
- https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md>
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md>
|
||||
|
||||
Additionally, you need to decide how you want to receive events from external sources like
|
||||
|
||||
@@ -74,33 +79,37 @@ Additionally, you need to decide how you want to receive events from external so
|
||||
|
||||
Set up your provider
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/catalogEventBasedProviders.ts
|
||||
+import { GithubOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-node';
|
||||
import { EventSubscriber } from '@backstage/plugin-events-node';
|
||||
import { PluginEnvironment } from '../types';
|
||||
export default async function createCatalogEventBasedProviders(
|
||||
- _: PluginEnvironment,
|
||||
+ env: PluginEnvironment,
|
||||
): Promise<Array<EntityProvider & EventSubscriber>> {
|
||||
const providers: Array<
|
||||
(EntityProvider & EventSubscriber) | Array<EntityProvider & EventSubscriber>
|
||||
> = [];
|
||||
- // add your event-based entity providers here
|
||||
+ providers.push(
|
||||
+ GithubOrgEntityProvider.fromConfig(env.config, {
|
||||
+ id: 'production',
|
||||
+ orgUrl: 'https://github.com/backstage',
|
||||
+ logger: env.logger,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { minutes: 60 },
|
||||
+ timeout: { minutes: 15 },
|
||||
+ }),
|
||||
+ }),
|
||||
+ );
|
||||
return providers.flat();
|
||||
}
|
||||
```ts title="packages/backend/src/plugins/catalogEventBasedProviders.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { GithubOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-node';
|
||||
import { EventSubscriber } from '@backstage/plugin-events-node';
|
||||
import { PluginEnvironment } from '../types';
|
||||
export default async function createCatalogEventBasedProviders(
|
||||
/* highlight-remove-next-line */
|
||||
_: PluginEnvironment,
|
||||
/* highlight-add-next-line */
|
||||
env: PluginEnvironment,
|
||||
): Promise<Array<EntityProvider & EventSubscriber>> {
|
||||
const providers: Array<
|
||||
(EntityProvider & EventSubscriber) | Array<EntityProvider & EventSubscriber>
|
||||
> = [];
|
||||
// add your event-based entity providers here
|
||||
/* highlight-add-start */
|
||||
providers.push(
|
||||
GithubOrgEntityProvider.fromConfig(env.config, {
|
||||
id: 'production',
|
||||
orgUrl: 'https://github.com/backstage',
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 60 },
|
||||
timeout: { minutes: 15 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
return providers.flat();
|
||||
}
|
||||
```
|
||||
|
||||
You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks).
|
||||
@@ -193,8 +202,7 @@ async (user, ctx): Promise<UserEntity | undefined> => {
|
||||
Once you have imported the emails you can resolve users in your sign-in in
|
||||
resolver using the catalog entity search via email
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/plugins/auth.ts
|
||||
```typescript title="packages/backend/src/plugins/auth.ts"
|
||||
ctx.signInWithCatalogUser({
|
||||
filter: {
|
||||
kind: ['User'],
|
||||
@@ -223,10 +231,9 @@ install and register it in the catalog plugin:
|
||||
yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-github
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
```typescript title="packages/backend/src/plugins/catalog.ts"
|
||||
import { GithubOrgReaderProcessor } from '@backstage/plugin-catalog-backend-module-github';
|
||||
// ...
|
||||
|
||||
builder.addProcessor(
|
||||
GithubOrgReaderProcessor.fromConfig(env.config, { logger: env.logger }),
|
||||
);
|
||||
|
||||
@@ -42,9 +42,7 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gitlab
|
||||
|
||||
Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`:
|
||||
|
||||
```ts
|
||||
/* packages/backend/src/plugins/catalog.ts */
|
||||
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
import { GitlabDiscoveryEntityProvider } from '@backstage/plugin-catalog-backend-module-gitlab';
|
||||
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
@@ -91,17 +89,22 @@ The target is composed of three parts:
|
||||
Finally, you will have to add the processor in the catalog initialization code
|
||||
of your backend.
|
||||
|
||||
```diff
|
||||
// In packages/backend/src/plugins/catalog.ts
|
||||
+import { GitLabDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-gitlab';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { GitLabDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-gitlab';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
+ builder.addProcessor(
|
||||
+ GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger })
|
||||
+ );
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-start */
|
||||
builder.addProcessor(
|
||||
GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
If you don't want create location object if file with component definition do not exists in project, you can set the `skipReposWithoutExactFileMatch` option. That can reduce count of request to gitlab with 404 status code.
|
||||
|
||||
@@ -35,28 +35,33 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-ldap
|
||||
Update the catalog plugin initialization in your backend to add the provider and
|
||||
schedule it:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
+import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap';
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
|
||||
+ // The target parameter below needs to match the ldap.providers.target
|
||||
+ // value specified in your app-config.
|
||||
+ builder.addEntityProvider(
|
||||
+ LdapOrgEntityProvider.fromConfig(env.config, {
|
||||
+ id: 'our-ldap-master',
|
||||
+ target: 'ldaps://ds.example.net',
|
||||
+ logger: env.logger,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { minutes: 60 },
|
||||
+ timeout: { minutes: 15 },
|
||||
+ }),
|
||||
+ }),
|
||||
+ );
|
||||
/* highlight-add-start */
|
||||
// The target parameter below needs to match the ldap.providers.target
|
||||
// value specified in your app-config.
|
||||
builder.addEntityProvider(
|
||||
LdapOrgEntityProvider.fromConfig(env.config, {
|
||||
id: 'our-ldap-master',
|
||||
target: 'ldaps://ds.example.net',
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 60 },
|
||||
timeout: { minutes: 15 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
After this, you also have to add some configuration in your app-config that
|
||||
@@ -345,8 +350,7 @@ frequency with which they are refreshed, separately from other processors.
|
||||
The `LdapOrgReaderProcessor` is not registered by default, so you have to
|
||||
register it in the catalog plugin:
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
```typescript title="packages/backend/src/plugins/catalog.ts"
|
||||
builder.addProcessor(
|
||||
LdapOrgReaderProcessor.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
|
||||
@@ -10,7 +10,7 @@ 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
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
|
||||
import { createConditionFactory } from '@backstage/plugin-permission-node';
|
||||
@@ -51,9 +51,7 @@ Now that we have a custom rule defined, we need provide it to the catalog plugin
|
||||
|
||||
The api for providing custom rules may differ between plugins, but there should typically be some integration point during the creation of the backend router. For the catalog, this integration point is exposed via `CatalogBuilder.addPermissionRules`.
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
|
||||
```typescript title="packages/backend/src/plugins/catalog.ts"
|
||||
import { isInSystemRule } from './permission';
|
||||
// The CatalogBuilder with the addPermissionRules function is in the alpha path
|
||||
import { CatalogBuilder } from '@backstage/plugin-catalog-backend/alpha';
|
||||
@@ -76,12 +74,9 @@ The new rule is now ready for use in a permission policy!
|
||||
|
||||
Let's bring this all together by extending the example policy from the previous section.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
|
||||
+ import { isInSystem } from './catalog';
|
||||
|
||||
...
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { isInSystem } from './catalog';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(
|
||||
@@ -91,17 +86,21 @@ class TestPermissionPolicy implements PermissionPolicy {
|
||||
if (isResourcePermission(request.permission, 'catalog-entity')) {
|
||||
return createCatalogConditionalDecision(
|
||||
request.permission,
|
||||
- catalogConditions.isEntityOwner({
|
||||
- claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
- }),
|
||||
+ {
|
||||
+ anyOf: [
|
||||
+ catalogConditions.isEntityOwner({
|
||||
+ claims: user?.identity.ownershipEntityRefs ?? []
|
||||
+ }),
|
||||
+ isInSystem('interviewing')
|
||||
+ ]
|
||||
+ }
|
||||
/* highlight-remove-start */
|
||||
catalogConditions.isEntityOwner({
|
||||
claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
}),
|
||||
/* highlight-remove-end */
|
||||
/* highlight-add-start */
|
||||
{
|
||||
anyOf: [
|
||||
catalogConditions.isEntityOwner({
|
||||
claims: user?.identity.ownershipEntityRefs ?? []
|
||||
}),
|
||||
isInSystem('interviewing')
|
||||
]
|
||||
}
|
||||
/* highlight-add-end */
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,27 +12,29 @@ However, there are some cases where the integrator needs to supplement the polic
|
||||
|
||||
If your Backstage permission policy may return a `DENY` for users requesting the `catalogEntityCreatePermission`, it may make sense, for example, to remove access to the `/catalog-import` page entirely:
|
||||
|
||||
```diff
|
||||
// packages/app/src/App.tsx
|
||||
|
||||
...
|
||||
|
||||
+ import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
|
||||
|
||||
...
|
||||
|
||||
- <Route path="/catalog-import" element={<CatalogImportPage />} />
|
||||
+ <Route
|
||||
+ path="/catalog-import"
|
||||
+ element={
|
||||
+ <RequirePermission permission={catalogEntityCreatePermission}>
|
||||
+ <CatalogImportPage />
|
||||
+ </RequirePermission>
|
||||
+ }
|
||||
+ />
|
||||
...
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-start */
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
{/* highlight-remove-next-line */}
|
||||
<Route path="/catalog-import" element={<CatalogImportPage />} />
|
||||
{/* highlight-add-start */}
|
||||
<Route
|
||||
path="/catalog-import"
|
||||
element={
|
||||
<RequirePermission permission={catalogEntityCreatePermission}>
|
||||
<CatalogImportPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
{/* highlight-add-end */}
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
With this change, users who are denied the `catalogEntityCreatePermission` should now be unable to access the `/catalog-import` page.
|
||||
|
||||
@@ -46,64 +46,67 @@ The permissions framework uses a new `permission-backend` plugin to accept autho
|
||||
|
||||
1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @backstage/plugin-permission-backend
|
||||
```
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @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 { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
discovery: env.discovery,
|
||||
policy: new TestPermissionPolicy(),
|
||||
identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
discovery: env.discovery,
|
||||
policy: new TestPermissionPolicy(),
|
||||
identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. Wire up the permission policy in `packages/backend/src/index.ts`. [The index in the example backend](https://github.com/backstage/backstage/blob/master/packages/backend/src/index.ts) shows how to do this. You’ll need to import the module from the previous step, create a plugin environment, and add the router to the express app:
|
||||
|
||||
```diff
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import search from './plugins/search';
|
||||
+ import permission from './plugins/permission';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import search from './plugins/search';
|
||||
/* highlight-add-next-line */
|
||||
import permission from './plugins/permission';
|
||||
|
||||
...
|
||||
async function main() {
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
|
||||
// ..
|
||||
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
+ const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
|
||||
|
||||
...
|
||||
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
+ apiRouter.use('/permission', await permission(permissionEnv));
|
||||
```
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Enable and test the permissions system
|
||||
|
||||
@@ -111,40 +114,46 @@ Now that the permission backend is running, it’s time to enable the permission
|
||||
|
||||
1. Set the property `permission.enabled` to `true` in `app-config.yaml`.
|
||||
|
||||
```yaml
|
||||
permission:
|
||||
enabled: true
|
||||
```
|
||||
```yaml title="app-config.yaml"
|
||||
permission:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
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 { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
- import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
+ import {
|
||||
+ PermissionPolicy,
|
||||
+ PolicyQuery,
|
||||
+ } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
/* highlight-remove-next-line */
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-end */
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
- async handle(): Promise<PolicyDecision> {
|
||||
+ async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
+ if (request.permission.name === 'catalog.entity.delete') {
|
||||
+ return {
|
||||
+ result: AuthorizeResult.DENY,
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Now that you’ve made this change, you should find that the unregister entity menu option on the catalog entity page is disabled.
|
||||
|
||||
|
||||
@@ -18,94 +18,101 @@ The source code is available here:
|
||||
- [todo-list-backend](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list-backend)
|
||||
- [todo-list-common](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list-common)
|
||||
|
||||
1. Copy-paste the three folders into the plugins folder of your backstage application repository (removing the `example-` prefix from each folder) or run the following script from the root of your backstage application:
|
||||
1. Copy-paste the three folders into the plugins folder of your backstage application repository (removing the `example-` prefix from each folder) or run the following script from the root of your backstage application:
|
||||
|
||||
```bash
|
||||
$ cd $(mktemp -d)
|
||||
git clone --depth 1 --quiet --no-checkout --filter=blob:none https://github.com/backstage/backstage.git .
|
||||
git checkout master -- plugins/example-todo-list/
|
||||
git checkout master -- plugins/example-todo-list-backend/
|
||||
git checkout master -- plugins/example-todo-list-common/
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json
|
||||
for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done
|
||||
cd -
|
||||
```
|
||||
```bash
|
||||
$ cd $(mktemp -d)
|
||||
git clone --depth 1 --quiet --no-checkout --filter=blob:none https://github.com/backstage/backstage.git .
|
||||
git checkout master -- plugins/example-todo-list/
|
||||
git checkout master -- plugins/example-todo-list-backend/
|
||||
git checkout master -- plugins/example-todo-list-common/
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json
|
||||
for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done
|
||||
cd -
|
||||
```
|
||||
|
||||
The `plugins` directory of your project should now include `todo-list`, `todo-list-backend`, and `todo-list-common`.
|
||||
The `plugins` directory of your project should now include `todo-list`, `todo-list-backend`, and `todo-list-common`.
|
||||
|
||||
**Important**: if you are on **Windows**, make sure you have WSL and git installed on your machine before executing the script above.
|
||||
**Important**: if you are on **Windows**, make sure you have WSL and git installed on your machine before executing the script above.
|
||||
|
||||
2. Add these packages as dependencies for your Backstage app:
|
||||
2. Add these packages as dependencies for your Backstage app:
|
||||
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @internal/plugin-todo-list-backend @internal/plugin-todo-list-common
|
||||
$ yarn add --cwd packages/app @internal/plugin-todo-list
|
||||
```
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @internal/plugin-todo-list-backend @internal/plugin-todo-list-common
|
||||
$ yarn add --cwd packages/app @internal/plugin-todo-list
|
||||
```
|
||||
|
||||
3. Include the backend and frontend plugin in your application:
|
||||
3. Include the backend and frontend plugin in your application:
|
||||
|
||||
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
|
||||
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
|
||||
|
||||
```typescript
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```typescript title="packages/backend/src/plugins/todolist.ts"
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Apply the following changes to `packages/backend/src/index.ts`:
|
||||
Apply the following changes to `packages/backend/src/index.ts`:
|
||||
|
||||
```diff
|
||||
import techdocs from './plugins/techdocs';
|
||||
+ import todoList from './plugins/todolist';
|
||||
import search from './plugins/search';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import techdocs from './plugins/techdocs';
|
||||
/* highlight-add-next-line */
|
||||
import todoList from './plugins/todolist';
|
||||
import search from './plugins/search';
|
||||
|
||||
...
|
||||
async function main() {
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const todoListEnv = useHotMemoize(module, () => createEnv('todolist'));
|
||||
// ..
|
||||
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
+ const todoListEnv = useHotMemoize(module, () => createEnv('todolist'));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/todolist', await todoList(todoListEnv));
|
||||
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
|
||||
apiRouter.use(notFoundHandler());
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
...
|
||||
Apply the following changes to `packages/app/src/App.tsx`:
|
||||
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
+ apiRouter.use('/todolist', await todoList(todoListEnv));
|
||||
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
|
||||
apiRouter.use(notFoundHandler());
|
||||
```
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { TodoListPage } from '@internal/plugin-todo-list';
|
||||
|
||||
Apply the following changes to `packages/app/src/App.tsx`:
|
||||
|
||||
```diff
|
||||
+ import { TodoListPage } from '@internal/plugin-todo-list';
|
||||
|
||||
...
|
||||
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
+ <Route path="/todo-list" element={<TodoListPage />} />
|
||||
</FlatRoutes>
|
||||
```
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
{/* highlight-add-next-line */}
|
||||
<Route path="/todo-list" element={<TodoListPage />} />
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
Now if you start your application you should be able to reach the `/todo-list` page:
|
||||
|
||||
|
||||
@@ -14,19 +14,25 @@ We'll start by creating a new permission, and then we'll use the permission api
|
||||
|
||||
Let's navigate to the file `plugins/todo-list-common/src/permissions.ts` and add our first permission:
|
||||
|
||||
```diff
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
```ts title="plugins/todo-list-common/src/permissions.ts"
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
- export const tempExamplePermission = createPermission({
|
||||
- name: 'temp.example.noop',
|
||||
- attributes: {},
|
||||
+ export const todoListCreatePermission = createPermission({
|
||||
+ name: 'todo.list.create',
|
||||
+ attributes: { action: 'create' },
|
||||
});
|
||||
/* highlight-remove-start */
|
||||
export const tempExamplePermission = createPermission({
|
||||
name: 'temp.example.noop',
|
||||
attributes: {},
|
||||
/* highlight-remove-end */
|
||||
/* highlight-add-start */
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
/* highlight-add-end */
|
||||
});
|
||||
|
||||
- export const todoListPermissions = [tempExamplePermission];
|
||||
+ export const todoListPermissions = [todoListCreatePermission];
|
||||
/* highlight-remove-next-line */
|
||||
export const todoListPermissions = [tempExamplePermission];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission];
|
||||
```
|
||||
|
||||
For this tutorial, we've automatically exported all permissions from this file (see `plugins/todo-list-common/src/index.ts`).
|
||||
@@ -44,81 +50,86 @@ $ yarn workspace @internal/plugin-todo-list-backend \
|
||||
|
||||
Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
|
||||
```diff
|
||||
...
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
/* highlight-remove-start */
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { IdentityApi } from '@backstage/plugin-auth-node';
|
||||
/* highlight-remove-end */
|
||||
/* highlight-add-start */
|
||||
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';
|
||||
/* highlight-add-end */
|
||||
|
||||
- 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';
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
identity: IdentityApi;
|
||||
/* highlight-add-next-line */
|
||||
permissions: PermissionEvaluator;
|
||||
}
|
||||
|
||||
...
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
/* highlight-remove-next-line */
|
||||
const { logger, identity } = options;
|
||||
/* highlight-add-next-line */
|
||||
const { logger, identity, permissions } = options;
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
identity: IdentityApi;
|
||||
+ permissions: PermissionEvaluator;
|
||||
}
|
||||
router.post('/todos', async (req, res) => {
|
||||
let author: string | undefined = undefined;
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
- const { logger, identity } = options;
|
||||
+ const { logger, identity, permissions } = options;
|
||||
const user = await identity.getIdentity({ request: req });
|
||||
author = user?.identity.userEntityRef;
|
||||
/* highlight-add-start */
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
const decision = (
|
||||
await permissions.authorize([{ permission: todoListCreatePermission }], {
|
||||
token,
|
||||
})
|
||||
)[0];
|
||||
|
||||
...
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
router.post('/todos', async (req, res) => {
|
||||
let author: string | undefined = undefined;
|
||||
if (!isTodoCreateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
|
||||
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,
|
||||
+ })
|
||||
+ )[0];
|
||||
|
||||
+ if (decision.result === AuthorizeResult.DENY) {
|
||||
+ throw new NotAllowedError('Unauthorized');
|
||||
+ }
|
||||
|
||||
if (!isTodoCreateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
|
||||
const todo = add({ title: req.body.title, author });
|
||||
res.json(todo);
|
||||
});
|
||||
const todo = add({ title: req.body.title, author });
|
||||
res.json(todo);
|
||||
});
|
||||
```
|
||||
|
||||
Pass the `permissions` object to the plugin in `packages/backend/src/plugins/todolist.ts`:
|
||||
|
||||
```diff
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```ts title="packages/backend/src/plugins/todolist.ts"
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
/* highlight-add-next-line */
|
||||
permissions,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
discovery,
|
||||
+ permissions,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
+ permissions,
|
||||
});
|
||||
}
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
/* highlight-add-next-line */
|
||||
permissions,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
That's it! Now your plugin is fully configured. Let's try to test the logic by denying the permission.
|
||||
@@ -129,200 +140,219 @@ Before running this step, please make sure you followed the steps described in [
|
||||
|
||||
In order to test the logic above, the integrators of your backstage instance need to change their permission policy to return `DENY` for our newly-created permission:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-end */
|
||||
import {
|
||||
PermissionPolicy,
|
||||
/* highlight-add-next-line */
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import { isPermission } from '@backstage/plugin-permission-common';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
+ import {
|
||||
+ BackstageIdentityResponse,
|
||||
+ } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
PermissionPolicy,
|
||||
+ PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
+ import { isPermission } from '@backstage/plugin-permission-common';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
- async handle(): Promise<PolicyDecision> {
|
||||
+ async handle(
|
||||
+ request: PolicyQuery,
|
||||
+ _user?: BackstageIdentityResponse,
|
||||
+ ): Promise<PolicyDecision> {
|
||||
+ if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
+ return {
|
||||
+ result: AuthorizeResult.DENY,
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
_user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Now the frontend should show an error whenever you try to create a new Todo item.
|
||||
|
||||
Let's flip the result back to `ALLOW` before moving on.
|
||||
|
||||
```diff
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
- result: AuthorizeResult.DENY,
|
||||
+ result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```ts
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
/* highlight-remove-next-line */
|
||||
result: AuthorizeResult.DENY,
|
||||
/* highlight-add-next-line */
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
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';
|
||||
```ts title="plugins/todo-list-backend/src/service/router.test.ts"
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-next-line */
|
||||
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { createRouter } from './router';
|
||||
import { createRouter } from './router';
|
||||
|
||||
+ const mockedAuthorize: jest.MockedFunction<PermissionEvaluator['authorize']> =
|
||||
+ jest.fn();
|
||||
+ const mockedPermissionQuery: jest.MockedFunction<
|
||||
+ PermissionEvaluator['authorizeConditional']
|
||||
+ > = jest.fn();
|
||||
/* highlight-add-start */
|
||||
const mockedAuthorize: jest.MockedFunction<PermissionEvaluator['authorize']> =
|
||||
jest.fn();
|
||||
const mockedPermissionQuery: jest.MockedFunction<
|
||||
PermissionEvaluator['authorizeConditional']
|
||||
> = jest.fn();
|
||||
|
||||
+ const permissionEvaluator: PermissionEvaluator = {
|
||||
+ authorize: mockedAuthorize,
|
||||
+ authorizeConditional: mockedPermissionQuery,
|
||||
+ };
|
||||
const permissionEvaluator: PermissionEvaluator = {
|
||||
authorize: mockedAuthorize,
|
||||
authorizeConditional: mockedPermissionQuery,
|
||||
};
|
||||
/* highlight-add-end */
|
||||
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
logger: getVoidLogger(),
|
||||
identity: {} as DefaultIdentityClient,
|
||||
+ permissions: permissionEvaluator,
|
||||
});
|
||||
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' });
|
||||
});
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
logger: getVoidLogger(),
|
||||
identity: {} as DefaultIdentityClient,
|
||||
/* highlight-add-next-line */
|
||||
permissions: permissionEvaluator,
|
||||
});
|
||||
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';
|
||||
```ts title="plugins/todo-list-backend/src/service/standaloneServer.ts"
|
||||
import {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
SingleHostDiscovery,
|
||||
/* highlight-add-next-line */
|
||||
ServerTokenManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-next-line */
|
||||
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 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);
|
||||
/* highlight-add-start */
|
||||
const tokenManager = ServerTokenManager.fromConfig(config, {
|
||||
logger,
|
||||
});
|
||||
const permissions = ServerPermissionClient.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
});
|
||||
/* highlight-add-end */
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
/* highlight-add-next-line */
|
||||
permissions,
|
||||
});
|
||||
|
||||
let service = createServiceBuilder(module)
|
||||
.setPort(options.port)
|
||||
.addRouter('/todo-list', router);
|
||||
if (options.enableCors) {
|
||||
service = service.enableCors({ origin: 'http://localhost:3000' });
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
module.hot?.accept();
|
||||
```
|
||||
|
||||
Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`:
|
||||
|
||||
```diff
|
||||
import { loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createRouter } from './service/router';
|
||||
```ts title="plugins/todo-list-backend/src/plugin.ts"
|
||||
import { loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createRouter } from './service/router';
|
||||
|
||||
/**
|
||||
* The example TODO list backend plugin.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const exampleTodoListPlugin = createBackendPlugin({
|
||||
pluginId: 'exampleTodoList',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
identity: coreServices.identity,
|
||||
logger: coreServices.logger,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
+ permissions: coreServices.permissions,
|
||||
},
|
||||
- async init({ identity, logger, httpRouter }) {
|
||||
+ async init({ identity, logger, httpRouter, permissions }) {
|
||||
httpRouter.use(
|
||||
await createRouter({
|
||||
identity,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
permissions,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/**
|
||||
* The example TODO list backend plugin.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const exampleTodoListPlugin = createBackendPlugin({
|
||||
pluginId: 'exampleTodoList',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
identity: coreServices.identity,
|
||||
logger: coreServices.logger,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
/* highlight-add-next-line */
|
||||
permissions: coreServices.permissions,
|
||||
},
|
||||
/* highlight-remove-next-line */
|
||||
async init({ identity, logger, httpRouter }) {
|
||||
/* highlight-add-next-line */
|
||||
async init({ identity, logger, httpRouter, permissions }) {
|
||||
httpRouter.use(
|
||||
await createRouter({
|
||||
identity,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
permissions,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Now when you run `yarn tsc` you should have no more errors.
|
||||
|
||||
@@ -10,24 +10,32 @@ When performing updates (or other operations) on specific [resources](../concept
|
||||
|
||||
Let's add a new permission to the file `plugins/todo-list-common/src/permissions.ts` from [the previous section](./02-adding-a-basic-permission-check.md).
|
||||
|
||||
```diff
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
```ts title="plugins/todo-list-common/src/permissions.ts"
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
+ export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
+
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
+
|
||||
+ export const todoListUpdatePermission = createPermission({
|
||||
+ name: 'todo.list.update',
|
||||
+ attributes: { action: 'update' },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
+ });
|
||||
/* highlight-add-next-line */
|
||||
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
|
||||
- export const todoListPermissions = [todoListCreatePermission];
|
||||
+ export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
|
||||
/* highlight-add-start */
|
||||
export const todoListUpdatePermission = createPermission({
|
||||
name: 'todo.list.update',
|
||||
attributes: { action: 'update' },
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
/* highlight-remove-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
];
|
||||
```
|
||||
|
||||
Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource.
|
||||
@@ -36,35 +44,42 @@ Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` pe
|
||||
|
||||
To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the same manner as we did in the previous section:
|
||||
|
||||
```diff
|
||||
- import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
+ import { todoListCreatePermission, todoListUpdatePermission } from '@internal/plugin-todo-list-common';
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
/* highlight-remove-next-line */
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-next-line */
|
||||
import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
router.put('/todos', async (req, res) => {
|
||||
/* highlight-add-start */
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
router.put('/todos', async (req, res) => {
|
||||
+ const token = getBearerTokenFromAuthorizationHeader(
|
||||
+ req.header('authorization'),
|
||||
+ );
|
||||
if (!isTodoUpdateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
/* highlight-add-start */
|
||||
const decision = (
|
||||
await permissions.authorize(
|
||||
[{ permission: todoListUpdatePermission, resourceRef: req.body.id }],
|
||||
{
|
||||
token,
|
||||
},
|
||||
)
|
||||
)[0];
|
||||
|
||||
if (!isTodoUpdateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
+ const decision = (
|
||||
+ await permissions.authorize(
|
||||
+ [{ permission: todoListUpdatePermission, resourceRef: req.body.id }],
|
||||
+ {
|
||||
+ token,
|
||||
+ },
|
||||
+ )
|
||||
+ )[0];
|
||||
+
|
||||
+ if (decision.result !== AuthorizeResult.ALLOW) {
|
||||
+ throw new NotAllowedError('Unauthorized');
|
||||
+ }
|
||||
if (decision.result !== AuthorizeResult.ALLOW) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
res.json(update(req.body));
|
||||
});
|
||||
res.json(update(req.body));
|
||||
});
|
||||
```
|
||||
|
||||
**Important:** Notice that we are passing an extra `resourceRef` field, with the `id` of the todo item as the value.
|
||||
@@ -75,13 +90,13 @@ This enables decisions based on characteristics of the resource, but it's import
|
||||
|
||||
Install the missing module:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node zod
|
||||
```
|
||||
|
||||
Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append the following code:
|
||||
|
||||
```typescript
|
||||
```typescript title="plugins/todo-list-backend/src/service/rules.ts"
|
||||
import { makeCreatePermissionRule } from '@backstage/plugin-permission-node';
|
||||
import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';
|
||||
import { z } from 'zod';
|
||||
@@ -130,35 +145,43 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser
|
||||
- `permissions`: the list of permissions that your plugin accepts.
|
||||
- `rules`: an array of all the permission rules you want to support in conditional decisions.
|
||||
|
||||
```diff
|
||||
...
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
/* highlight-remove-next-line */
|
||||
import { add, getAll, update } from './todos';
|
||||
/* highlight-add-start */
|
||||
import { add, getAll, getTodo, update } from './todos';
|
||||
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
import { TODO_LIST_RESOURCE_TYPE, todoListPermissions } from '@internal/plugin-todo-list-common';
|
||||
import { rules } from './rules';
|
||||
/* highlight-add-end */
|
||||
|
||||
- import { add, getAll, update } from './todos';
|
||||
+ import { add, getAll, getTodo, update } from './todos';
|
||||
+ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
+ import { TODO_LIST_RESOURCE_TYPE, todoListPermissions } from '@internal/plugin-todo-list-common';
|
||||
+ import { rules } from './rules';
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { logger, identity, permissions } = options;
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { logger, identity, permissions } = options;
|
||||
/* highlight-add-start */
|
||||
const permissionIntegrationRouter = createPermissionIntegrationRouter({
|
||||
getResources: async resourceRefs => {
|
||||
return resourceRefs.map(getTodo);
|
||||
},
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
permissions: todoListPermissions,
|
||||
rules: Object.values(rules),
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
+ const permissionIntegrationRouter = createPermissionIntegrationRouter({
|
||||
+ getResources: async resourceRefs => {
|
||||
+ return resourceRefs.map(getTodo);
|
||||
+ },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
+ permissions: todoListPermissions,
|
||||
+ rules: Object.values(rules),
|
||||
+ });
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
/* highlight-add-next-line */
|
||||
router.use(permissionIntegrationRouter);
|
||||
|
||||
+ router.use(permissionIntegrationRouter);
|
||||
|
||||
router.post('/todos', async (req, res) => {
|
||||
router.post('/todos', async (req, res) => {
|
||||
// ..
|
||||
}
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
## Provide utilities for policy authors
|
||||
@@ -167,7 +190,7 @@ Now that we have a new resource type and a corresponding rule, we need to export
|
||||
|
||||
Create a new `plugins/todo-list-backend/src/conditionExports.ts` file and add the following code:
|
||||
|
||||
```typescript
|
||||
```typescript title="plugins/todo-list-backend/src/conditionExports.ts"
|
||||
import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';
|
||||
import { createConditionExports } from '@backstage/plugin-permission-node';
|
||||
import { rules } from './service/rules';
|
||||
@@ -185,63 +208,68 @@ export const createTodoListConditionalDecision = createConditionalDecision;
|
||||
|
||||
Make sure `todoListConditions` and `createTodoListConditionalDecision` are exported from the `todo-list-backend` package by editing `plugins/todo-list-backend/src/index.ts`:
|
||||
|
||||
```diff
|
||||
export * from './service/router';
|
||||
+ export * from './conditionExports';
|
||||
export { exampleTodoListPlugin } from './plugin';
|
||||
```ts title="plugins/todo-list-backend/src/index.ts"
|
||||
export * from './service/router';
|
||||
/* highlight-add-next-line */
|
||||
export * from './conditionExports';
|
||||
export { exampleTodoListPlugin } from './plugin';
|
||||
```
|
||||
|
||||
## Test the authorized update endpoint
|
||||
|
||||
Let's go back to the permission policy's handle function and try to authorize our new permission with an `isOwner` condition.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
IdentityClient
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import { isPermission } from '@backstage/plugin-permission-common';
|
||||
/* highlight-remove-next-line */
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
import {
|
||||
todoListConditions,
|
||||
createTodoListConditionalDecision,
|
||||
} from '@internal/plugin-todo-list-backend';
|
||||
/* highlight-add-end */
|
||||
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
IdentityClient
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import { isPermission } from '@backstage/plugin-permission-common';
|
||||
- import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
+ import {
|
||||
+ todoListCreatePermission,
|
||||
+ todoListUpdatePermission,
|
||||
+ } from '@internal/plugin-todo-list-common';
|
||||
+ import {
|
||||
+ todoListConditions,
|
||||
+ createTodoListConditionalDecision,
|
||||
+ } from '@internal/plugin-todo-list-backend';
|
||||
|
||||
...
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
- _user?: BackstageIdentityResponse,
|
||||
+ user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
|
||||
+ if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
+ return createTodoListConditionalDecision(
|
||||
+ request.permission,
|
||||
+ todoListConditions.isOwner({
|
||||
+ userId: user?.identity.userEntityRef ?? '',
|
||||
+ }),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
/* highlight-remove-next-line */
|
||||
_user?: BackstageIdentityResponse,
|
||||
/* highlight-add-next-line */
|
||||
user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
/* highlight-add-start */
|
||||
if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
return createTodoListConditionalDecision(
|
||||
request.permission,
|
||||
todoListConditions.isOwner({
|
||||
userId: user?.identity.userEntityRef ?? '',
|
||||
}),
|
||||
);
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
For any incoming update requests, we now return a _Conditional Decision_. We are saying:
|
||||
|
||||
@@ -8,21 +8,28 @@ Authorizing `GET /todos` is similar to the update endpoint, in that it should be
|
||||
|
||||
One possible solution may leverage the batching functionality to authorize all of the todos, and then returning only the ones for which the decision was `ALLOW`:
|
||||
|
||||
```diff
|
||||
router.get('/todos', async (req, res) => {
|
||||
+ const token = IdentityClient.getBearerToken(req.header('authorization'));
|
||||
```ts
|
||||
router.get('/todos', async (req, res) => {
|
||||
/* highlight-add-next-line */
|
||||
const token = IdentityClient.getBearerToken(req.header('authorization'));
|
||||
|
||||
- res.json(getAll())
|
||||
+ const items = getAll();
|
||||
+ const decisions = await permissions.authorize(
|
||||
+ items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })),
|
||||
+ );
|
||||
/* highlight-remove-next-line */
|
||||
res.json(getAll());
|
||||
/* highlight-add-start */
|
||||
const items = getAll();
|
||||
const decisions = await permissions.authorize(
|
||||
items.map(({ id }) => ({
|
||||
permission: todoListReadPermission,
|
||||
resourceRef: id,
|
||||
})),
|
||||
);
|
||||
|
||||
+ const filteredItems = decisions.filter(
|
||||
+ decision => decision.result === AuthorizeResult.ALLOW,
|
||||
+ );
|
||||
+ res.json(filteredItems);
|
||||
});
|
||||
const filteredItems = decisions.filter(
|
||||
decision => decision.result === AuthorizeResult.ALLOW,
|
||||
);
|
||||
res.json(filteredItems);
|
||||
/* highlight-add-end */
|
||||
});
|
||||
```
|
||||
|
||||
This approach will work for simple cases, but it has a downside: it forces us to retrieve all the elements upfront and authorize them one by one. This forces the plugin implementation to handle concerns like pagination, which is currently handled by the data source.
|
||||
@@ -35,82 +42,100 @@ To avoid this situation, the permissions framework has support for filtering ite
|
||||
|
||||
Let's add another permission to the plugin.
|
||||
|
||||
```diff
|
||||
// plugins/todo-list-backend/src/service/permissions.ts
|
||||
```ts title="plugins/todo-list-backend/src/service/permissions.ts"
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
|
||||
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
export const todoListUpdatePermission = createPermission({
|
||||
name: 'todo.list.update',
|
||||
attributes: { action: 'update' },
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
});
|
||||
|
||||
export const todoListUpdatePermission = createPermission({
|
||||
name: 'todo.list.update',
|
||||
attributes: { action: 'update' },
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
});
|
||||
+
|
||||
+ export const todoListReadPermission = createPermission({
|
||||
+ name: 'todos.list.read',
|
||||
+ attributes: { action: 'read' },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
+ });
|
||||
/* highlight-add-start */
|
||||
export const todoListReadPermission = createPermission({
|
||||
name: 'todos.list.read',
|
||||
attributes: { action: 'read' },
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
- export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
+ export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
todoListReadPermission,
|
||||
];
|
||||
```
|
||||
|
||||
## Using conditional policy decisions
|
||||
|
||||
So far we've only used the `PermissionEvaluator.authorize` method, which will evaluate conditional decisions before returning a result. In this step, we want to evaluate conditional decisions within our plugin, so we'll use `PermissionEvaluator.authorizeConditional` instead.
|
||||
|
||||
```diff
|
||||
// plugins/todo-list-backend/src/service/router.ts
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
/* highlight-remove-next-line */
|
||||
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
createPermissionIntegrationRouter,
|
||||
createConditionTransformer,
|
||||
ConditionTransformer,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-end */
|
||||
/* highlight-remove-next-line */
|
||||
import { add, getAll, getTodo, update } from './todos';
|
||||
/* highlight-add-next-line */
|
||||
import { add, getAll, getTodo, TodoFilter, update } from './todos';
|
||||
import {
|
||||
todosListCreate,
|
||||
todosListUpdate,
|
||||
/* highlight-add-next-line */
|
||||
todoListReadPermission,
|
||||
TODO_LIST_RESOURCE_TYPE,
|
||||
} from './permissions';
|
||||
|
||||
- import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
+ import {
|
||||
+ createPermissionIntegrationRouter,
|
||||
+ createConditionTransformer,
|
||||
+ ConditionTransformer,
|
||||
+ } from '@backstage/plugin-permission-node';
|
||||
- import { add, getAll, getTodo, update } from './todos';
|
||||
+ import { add, getAll, getTodo, TodoFilter, update } from './todos';
|
||||
import {
|
||||
todosListCreate,
|
||||
todosListUpdate,
|
||||
+ todoListReadPermission,
|
||||
TODO_LIST_RESOURCE_TYPE,
|
||||
} from './permissions';
|
||||
/* highlight-add-next-line */
|
||||
const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules));
|
||||
|
||||
+ const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules));
|
||||
/* highlight-remove-next-line */
|
||||
router.get('/todos', async (_req, res) => {
|
||||
/* highlight-add-start */
|
||||
router.get('/todos', async (req, res) => {
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
|
||||
- router.get('/todos', async (_req, res) => {
|
||||
+ router.get('/todos', async (req, res) => {
|
||||
+ const token = getBearerTokenFromAuthorizationHeader(
|
||||
+ req.header('authorization'),
|
||||
+ );
|
||||
+
|
||||
+ const decision = (
|
||||
+ await permissions.authorizeConditional([{ permission: todoListReadPermission }], {
|
||||
+ token,
|
||||
+ })
|
||||
+ )[0];
|
||||
+
|
||||
+ if (decision.result === AuthorizeResult.DENY) {
|
||||
+ throw new NotAllowedError('Unauthorized');
|
||||
+ }
|
||||
+
|
||||
+ if (decision.result === AuthorizeResult.CONDITIONAL) {
|
||||
+ const filter = transformConditions(decision.conditions);
|
||||
+ res.json(getAll(filter));
|
||||
+ } else {
|
||||
+ res.json(getAll());
|
||||
+ }
|
||||
- res.json(getAll());
|
||||
});
|
||||
const decision = (
|
||||
await permissions.authorizeConditional([{ permission: todoListReadPermission }], {
|
||||
token,
|
||||
})
|
||||
)[0];
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
|
||||
if (decision.result === AuthorizeResult.CONDITIONAL) {
|
||||
const filter = transformConditions(decision.conditions);
|
||||
res.json(getAll(filter));
|
||||
} else {
|
||||
res.json(getAll());
|
||||
}
|
||||
/* highlight-add-end */
|
||||
/* highlight-remove-next-line */
|
||||
res.json(getAll());
|
||||
});
|
||||
```
|
||||
|
||||
To make the process of handling conditional decisions easier, the permission framework provides a `createConditionTransformer` helper. This function accepts an array of permission rules, and returns a transformer function which converts the conditions to the format needed by the plugin using the `toQuery` method defined on each rule.
|
||||
@@ -121,31 +146,29 @@ Since `TodoFilter` used in our plugin matches the structure of the conditions ob
|
||||
|
||||
Let's update our permission policy to return a conditional result whenever a `todoListReadPermission` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
|
||||
...
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
+ todoListReadPermission,
|
||||
/* highlight-add-next-line */
|
||||
todoListReadPermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
- if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
+ if (
|
||||
+ isPermission(request.permission, todoListUpdatePermission) ||
|
||||
+ isPermission(request.permission, todoListReadPermission)
|
||||
+ ) {
|
||||
return createTodoListConditionalDecision(
|
||||
request.permission,
|
||||
todoListConditions.isOwner({
|
||||
userId: user?.identity.userEntityRef
|
||||
}),
|
||||
);
|
||||
}
|
||||
/* highlight-remove-next-line */
|
||||
if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
/* highlight-add-start */
|
||||
if (
|
||||
isPermission(request.permission, todoListUpdatePermission) ||
|
||||
isPermission(request.permission, todoListReadPermission)
|
||||
) {
|
||||
/* highlight-add-end */
|
||||
return createTodoListConditionalDecision(
|
||||
request.permission,
|
||||
todoListConditions.isOwner({
|
||||
userId: user?.identity.userEntityRef
|
||||
}),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Once the changes to the permission policy are saved, the UI should show only the todo items you've created.
|
||||
|
||||
@@ -14,81 +14,80 @@ Take, for example, the "Add" button in our todo list application. When a user cl
|
||||
|
||||
Let's start by adding the packages we will need:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ yarn workspace @internal/plugin-todo-list \
|
||||
add @backstage/plugin-permission-react @internal/plugin-todo-list-common
|
||||
```
|
||||
|
||||
Let's make the following changes in `plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx`:
|
||||
|
||||
```diff
|
||||
...
|
||||
```tsx title="plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx"
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
/* highlight-add-start */
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
+ import { usePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
/* highlight-add-next-line */
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({
|
||||
permission: todoListCreatePermission,
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
+ const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
- <Button variant="contained" onClick={handleAdd}>
|
||||
- Add
|
||||
- </Button>
|
||||
+ {!loadingPermission && (
|
||||
+ <Button
|
||||
+ disabled={!canAddTodo}
|
||||
+ variant="contained"
|
||||
+ onClick={() => onAdd(title.current)}
|
||||
+ >
|
||||
+ Add
|
||||
+ </Button>
|
||||
+ )}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
...
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
{/* highlight-remove-start */}
|
||||
<Button variant="contained" onClick={handleAdd}>
|
||||
Add
|
||||
</Button>
|
||||
{/* highlight-remove-end */}
|
||||
{/* highlight-add-start */}
|
||||
{!loadingPermission && (
|
||||
<Button
|
||||
disabled={!canAddTodo}
|
||||
variant="contained"
|
||||
onClick={() => onAdd(title.current)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
{/* highlight-add-end */}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Here we are using the [`usePermission` hook](https://backstage.io/docs/reference/plugin-permission-react.usepermission) to communicate with the permission policy and receive a decision on whether this user is authorized to create a todo list item.
|
||||
|
||||
It's really that simple! Let's change our policy to test the disabled button:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
|
||||
...
|
||||
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
- result: AuthorizeResult.ALLOW,
|
||||
+ result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
|
||||
...
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
/* highlight-remove-next-line */
|
||||
result: AuthorizeResult.ALLOW,
|
||||
/* highlight-add-next-line */
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
And now you should see that you are not able to create a todo item from the frontend!
|
||||
@@ -97,104 +96,110 @@ And now you should see that you are not able to create a todo item from the fron
|
||||
|
||||
Providing a disabled state can be a helpful signal to users, but there may be cases where hiding the element is preferred. For such cases, you can use the provided [`RequirePermission` component](https://backstage.io/docs/reference/plugin-permission-react.requirepermission):
|
||||
|
||||
```diff
|
||||
// plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx
|
||||
```tsx title="plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx"
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
/* highlight-remove-next-line */
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
/* highlight-add-next-line */
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
export const TodoListPage = () => {
|
||||
// ..
|
||||
<Grid container spacing={3} direction="column">
|
||||
{/* highlight-remove-start */}
|
||||
<Grid item>
|
||||
<AddTodo onAdd={handleAdd} />
|
||||
</Grid>
|
||||
{/* highlight-remove-end */}
|
||||
{/* highlight-add-start */}
|
||||
<RequirePermission permission={todoListCreatePermission} errorPage={<></>}>
|
||||
<Grid item>
|
||||
<AddTodo onAdd={handleAdd} />
|
||||
</Grid>
|
||||
</RequirePermission>
|
||||
{/* highlight-add-end */}
|
||||
<Grid item>
|
||||
<TodoList key={key} onEdit={setEdit} />
|
||||
</Grid>
|
||||
</Grid>;
|
||||
};
|
||||
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
- import { usePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
/* highlight-remove-next-line */
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({
|
||||
permission: todoListCreatePermission,
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
export const TodoListPage = () => {
|
||||
|
||||
...
|
||||
|
||||
<Grid container spacing={3} direction="column">
|
||||
- <Grid item>
|
||||
- <AddTodo onAdd={handleAdd} />
|
||||
- </Grid>
|
||||
+ <RequirePermission
|
||||
+ permission={todoListCreatePermission}
|
||||
+ errorPage={<></>}
|
||||
+ >
|
||||
+ <Grid item>
|
||||
+ <AddTodo onAdd={handleAdd} />
|
||||
+ </Grid>
|
||||
+ </RequirePermission>
|
||||
<Grid item>
|
||||
<TodoList key={key} onEdit={setEdit} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
...
|
||||
|
||||
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
- const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
- {!loadingPermission && (
|
||||
- <Button
|
||||
- disabled={!canAddTodo}
|
||||
- variant="contained"
|
||||
- onClick={() => onAdd(title.current)}
|
||||
- >
|
||||
- Add
|
||||
- </Button>
|
||||
- )}
|
||||
+ <Button variant="contained" onClick={() => onAdd(title.current)}>
|
||||
+ Add
|
||||
+ </Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
...
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
{/* highlight-remove-start */}
|
||||
{!loadingPermission && (
|
||||
<Button
|
||||
disabled={!canAddTodo}
|
||||
variant="contained"
|
||||
onClick={() => onAdd(title.current)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
{/* highlight-remove-end */}
|
||||
{/* highlight-add-start */}
|
||||
<Button variant="contained" onClick={() => onAdd(title.current)}>
|
||||
Add
|
||||
</Button>
|
||||
{/* highlight-add-end */}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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';
|
||||
|
||||
...
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-start */
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<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>
|
||||
{/* highlight-add-next-line */}
|
||||
<Route path="/todo-list" element={
|
||||
{/* You might want to create a "read" permission for this, we are just using this one as an example */}
|
||||
{/* highlight-add-start */}
|
||||
<RequirePermission permission={todoListCreatePermission}>
|
||||
<TodoListPage />
|
||||
</RequirePermission>
|
||||
{/* highlight-add-end */}}
|
||||
{/* ... */}
|
||||
</Route>
|
||||
</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.
|
||||
|
||||
@@ -8,9 +8,7 @@ In the [previous section](./getting-started.md), we were able to set up the perm
|
||||
|
||||
That policy looked like this:
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
@@ -36,49 +34,61 @@ As we confirmed in the previous section, we know that this now prevents us from
|
||||
|
||||
Let's change the policy to the following:
|
||||
|
||||
```diff
|
||||
- import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
+ import {
|
||||
+ BackstageIdentityResponse,
|
||||
+ IdentityClient
|
||||
+ } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
```ts
|
||||
/* highlight-remove-next-line */
|
||||
import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
IdentityClient
|
||||
} from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-end */
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
+ isPermission,
|
||||
/* highlight-add-next-line */
|
||||
isPermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
+ import {
|
||||
+ catalogConditions,
|
||||
+ createCatalogConditionalDecision,
|
||||
+ } from '@backstage/plugin-catalog-backend/alpha';
|
||||
+ import {
|
||||
+ catalogEntityDeletePermission,
|
||||
+ } from '@backstage/plugin-catalog-common/alpha';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
catalogConditions,
|
||||
createCatalogConditionalDecision,
|
||||
} from '@backstage/plugin-catalog-backend/alpha';
|
||||
import {
|
||||
catalogEntityDeletePermission,
|
||||
} from '@backstage/plugin-catalog-common/alpha';
|
||||
/* highlight-add-end */
|
||||
|
||||
...
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
- async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
+ async handle(
|
||||
+ request: PolicyQuery,
|
||||
+ user?: BackstageIdentityResponse,
|
||||
+ ): Promise<PolicyDecision> {
|
||||
- if (request.permission.name === 'catalog.entity.delete') {
|
||||
+ if (isPermission(request.permission, catalogEntityDeletePermission)) {
|
||||
- return {
|
||||
- result: AuthorizeResult.DENY,
|
||||
- };
|
||||
+ return createCatalogConditionalDecision(
|
||||
+ request.permission,
|
||||
+ catalogConditions.isEntityOwner({
|
||||
+ claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
+ }),
|
||||
+ );
|
||||
}
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
/* highlight-add-end */
|
||||
/* highlight-remove-next-line */
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
/* highlight-add-next-line */
|
||||
if (isPermission(request.permission, catalogEntityDeletePermission)) {
|
||||
/* highlight-remove-start */
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
/* highlight-remove-end */
|
||||
/* highlight-add-start */
|
||||
return createCatalogConditionalDecision(
|
||||
request.permission,
|
||||
catalogConditions.isEntityOwner({
|
||||
claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
}
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's walk through the new code that we just added.
|
||||
@@ -93,30 +103,34 @@ You should now be able to see in your Backstage app that the unregister entity b
|
||||
|
||||
Now let's say we want to prevent all actions on catalog entities unless performed by the owner. One way to achieve this may be to simply update the `if` statement and check for each permission. If you choose to write your policy this way, it will certainly work! However, it may be difficult to maintain as the policy grows, and it may not be obvious if certain permissions are left out. We can author this same policy in a more scalable way by checking the resource type of the requested permission.
|
||||
|
||||
```diff
|
||||
```ts
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
- isPermission,
|
||||
+ isResourcePermission,
|
||||
/* highlight-remove-next-line */
|
||||
isPermission,
|
||||
isResourcePermission,
|
||||
/* highlight-add-next-line */
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
catalogConditions,
|
||||
createCatalogConditionalDecision,
|
||||
} from '@backstage/plugin-catalog-backend/alpha';
|
||||
- import {
|
||||
- catalogEntityDeletePermission,
|
||||
- } from '@backstage/plugin-catalog-common/alpha';
|
||||
|
||||
...
|
||||
import {
|
||||
catalogConditions,
|
||||
createCatalogConditionalDecision,
|
||||
} from '@backstage/plugin-catalog-backend/alpha';
|
||||
/* highlight-remove-start */
|
||||
import {
|
||||
catalogEntityDeletePermission,
|
||||
} from '@backstage/plugin-catalog-common/alpha';
|
||||
/* highlight-remove-end */
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
- if (isPermission(request.permission, catalogEntityDeletePermission)) {
|
||||
+ if (isResourcePermission(request.permission, 'catalog-entity')) {
|
||||
/* highlight-remove-next-line */
|
||||
if (isPermission(request.permission, catalogEntityDeletePermission)) {
|
||||
/* highlight-add-next-line */
|
||||
if (isResourcePermission(request.permission, 'catalog-entity')) {
|
||||
return createCatalogConditionalDecision(
|
||||
request.permission,
|
||||
catalogConditions.isEntityOwner({
|
||||
@@ -127,6 +141,7 @@ class TestPermissionPolicy implements PermissionPolicy {
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, we use [`isResourcePermission`](https://backstage.io/docs/reference/plugin-permission-common.isresourcepermission) to match all permissions with a resource type of `catalog-entity`. Just like `isPermission`, this helper will "narrow" the type of `request.permission` and enable the use of `createCatalogConditionalDecision`. In addition to the behavior you observed before, you should also see that catalog entities are no longer visible unless you are the owner - success!
|
||||
|
||||
@@ -38,10 +38,13 @@ across your project. This will automatically convert all module imports in your
|
||||
source code to use one of the three new core packages instead. For example, the
|
||||
following change might occur:
|
||||
|
||||
```diff
|
||||
-import { useApi, configApiRef, InfoCard } from '@backstage/core';
|
||||
+import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
+import { InfoCard } from '@backstage/core-components';
|
||||
```ts
|
||||
/* highlight-remove-next-line */
|
||||
import { useApi, configApiRef, InfoCard } from '@backstage/core';
|
||||
/* highlight-add-start */
|
||||
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { InfoCard } from '@backstage/core-components';
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
In a typical app created with `@backstage/create-app`, you would run the
|
||||
|
||||
@@ -45,7 +45,7 @@ yarn backstage-cli migrate react-router-deps
|
||||
|
||||
For those interested in doing this manually, apply the below change to all `package.json` files except the one at `packages/app/package.json` or any other app packages. Skip moving any dependencies that don't already exist, and move both `dependencies` and `devDependencies`.
|
||||
|
||||
```diff
|
||||
```diff title="package.json"
|
||||
dependencies {
|
||||
...
|
||||
- "react-router-dom": "^6.0.0-beta.0",
|
||||
@@ -64,13 +64,13 @@ It's important that you also update your external plugins to their latest versio
|
||||
|
||||
During this migration there may be external plugins that need updating. If you encounter any plugins outside of the `@backstage` scope that are incompatible with your installation, make sure to check for an existing issue or raise a new one at the plugin's GitHub repository.
|
||||
|
||||
### Step 4 - Bump the React Router dependencies in your app.
|
||||
### Step 4 - Bump the React Router dependencies in your app
|
||||
|
||||
Now it's time to do the actual migration to the latest version of React Router. At this time of writing that is `6.3.0`, but that is of course a moving target.
|
||||
|
||||
The first step is to modify `packages/app/package.json`:
|
||||
|
||||
```diff
|
||||
```diff title="package.json"
|
||||
- "react-router": "6.0.0-beta.0",
|
||||
- "react-router-dom": "6.0.0-beta.0",
|
||||
+ "react-router": "^6.3.0",
|
||||
@@ -81,7 +81,7 @@ In case you happen to have multiple app packages in your project, apply the same
|
||||
|
||||
Once the change has been made, run `yarn install`, and then `yarn why react-router` to validate the installation. You should see the following line in the log as the only resulting entry:
|
||||
|
||||
```
|
||||
```bash
|
||||
=> Found "react-router@6.3.0"
|
||||
```
|
||||
|
||||
@@ -157,28 +157,38 @@ Because of the above change, the `PermissionedRoute` component no longer works i
|
||||
|
||||
It's crucial that you update to `RequirePermission` at the same time as you update to React Router v6 stable as the `PermissionedRoute` component will no longer function.
|
||||
|
||||
```diff
|
||||
- <PermissionedRoute
|
||||
- path="/catalog-import"
|
||||
- permission={catalogEntityCreatePermission}
|
||||
- element={<CatalogImportPage />}
|
||||
+ <Route
|
||||
+ path="/catalog-import"
|
||||
+ element={
|
||||
+ <RequirePermission permission={catalogEntityCreatePermission}>
|
||||
+ <CatalogImportPage />
|
||||
+ </RequirePermission>
|
||||
+ }
|
||||
/>
|
||||
```tsx
|
||||
{/* highlight-remove-start */}
|
||||
<PermissionedRoute
|
||||
path="/catalog-import"
|
||||
permission={catalogEntityCreatePermission}
|
||||
element={<CatalogImportPage />}
|
||||
{/* highlight-remove-end */}
|
||||
{/* highlight-add-start */}
|
||||
<Route
|
||||
path="/catalog-import"
|
||||
element={
|
||||
<RequirePermission permission={catalogEntityCreatePermission}>
|
||||
<CatalogImportPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
{/* highlight-add-end */}
|
||||
/>
|
||||
```
|
||||
|
||||
### `<Navigate />` component
|
||||
|
||||
When migrating over to React Router v6 stable, you might also see browser console warnings for the `Navigate` component. This will need to be wrapped up in a `Route` component with the `Navigate` component in the `element` prop.
|
||||
|
||||
```diff
|
||||
- <Navigate key="/" to="catalog" />
|
||||
+ <Route path="/" element={<Navigate to="catalog" />} />
|
||||
```tsx
|
||||
{
|
||||
/* highlight-remove-next-line */
|
||||
}
|
||||
<Navigate key="/" to="catalog" />;
|
||||
{
|
||||
/* highlight-add-next-line */
|
||||
}
|
||||
<Route path="/" element={<Navigate to="catalog" />} />;
|
||||
```
|
||||
|
||||
### `NavLink`
|
||||
|
||||
@@ -29,24 +29,28 @@ yarn add --cwd packages/backend pg
|
||||
Next, modify `app-config.yaml` in the root folder to add PostgreSQL
|
||||
configuration for the backend:
|
||||
|
||||
```diff
|
||||
```yaml title="app-config.yaml"
|
||||
backend:
|
||||
database:
|
||||
- client: better-sqlite3
|
||||
- connection: ':memory:'
|
||||
+ # config options: https://node-postgres.com/api/client
|
||||
+ client: pg
|
||||
+ connection:
|
||||
+ host: ${POSTGRES_HOST}
|
||||
+ 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
|
||||
# highlight-remove-start
|
||||
client: better-sqlite3
|
||||
connection: ':memory:'
|
||||
# highlight-remove-end
|
||||
# highlight-add-start
|
||||
# config options: https://node-postgres.com/api/client
|
||||
client: pg
|
||||
connection:
|
||||
host: ${POSTGRES_HOST}
|
||||
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
|
||||
# highlight-add-end
|
||||
```
|
||||
|
||||
If you have an `app-config.local.yaml` for local development, a similar update
|
||||
@@ -60,31 +64,35 @@ The Backstage App is now ready to start up with a PostgreSQL backing database.
|
||||
|
||||
If you want to override the default connection pool settings then use the below configuration:
|
||||
|
||||
```diff
|
||||
```yaml title="app-config.local.yaml"
|
||||
backend:
|
||||
database:
|
||||
- client: better-sqlite3
|
||||
- connection: ':memory:'
|
||||
+ # config options: https://node-postgres.com/api/client
|
||||
+ client: pg
|
||||
+ connection:
|
||||
+ host: ${POSTGRES_HOST}
|
||||
+ 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:
|
||||
+ min: 3
|
||||
+ max: 12
|
||||
+ acquireTimeoutMillis: 60000
|
||||
+ idleTimeoutMillis: 60000
|
||||
# highlight-remove-start
|
||||
client: better-sqlite3
|
||||
connection: ':memory:'
|
||||
# highlight-remove-end
|
||||
# highlight-add-start
|
||||
# config options: https://node-postgres.com/api/client
|
||||
client: pg
|
||||
connection:
|
||||
host: ${POSTGRES_HOST}
|
||||
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:
|
||||
min: 3
|
||||
max: 12
|
||||
acquireTimeoutMillis: 60000
|
||||
idleTimeoutMillis: 60000
|
||||
# highlight-add-end
|
||||
```
|
||||
|
||||
### Using a single database
|
||||
|
||||
@@ -82,19 +82,24 @@ RUN yarn workspaces focus --all --production && rm -rf "$(yarn cache clean)"
|
||||
|
||||
Additionally, `yarn config` has been reworked from being able to store any arbitrary key-value pairs to only supporting a handful of predefined pairs. Previously, we would set our preferred `python3` interpreter to work around [any issues related to node-gyp](https://github.com/backstage/backstage/issues/11583) so we need to provide an appropriate substitute.
|
||||
|
||||
```diff
|
||||
```Dockerfile
|
||||
FROM node:16-bullseye-slim
|
||||
|
||||
+# Set Python interpreter for `node-gyp` to use
|
||||
+ENV PYTHON /usr/bin/python3
|
||||
# highlight-add-start
|
||||
# Set Python interpreter for `node-gyp` to use
|
||||
ENV PYTHON /usr/bin/python3
|
||||
# highlight-add-end
|
||||
|
||||
# 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 && \
|
||||
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
|
||||
+ rm -rf /var/lib/apt/lists/*
|
||||
# highlight-remove-start
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
yarn config set python /usr/bin/python3
|
||||
# highlight-remove-end
|
||||
# highlight-add-next-line
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
```
|
||||
|
||||
You'll want to make sure that the `PYTHON` environment variable is declared relatively early, before any instances of `Yarn` are invoked as `node-gyp` is indirectly triggered by some modules during installation.
|
||||
|
||||
Reference in New Issue
Block a user