Merge remote-tracking branch 'origin/master' into retry-and-recovery-doc
This commit is contained in:
@@ -7,20 +7,17 @@ description: Installing Kubernetes plugin into Backstage
|
||||
The Kubernetes feature is a plugin to Backstage, and it is exposed as a tab when
|
||||
viewing entities in the software catalog.
|
||||
|
||||
If you haven't setup Backstage already, read the
|
||||
[Getting Started](../../getting-started/index.md) guide.
|
||||
If you haven't set up Backstage already, read the [Getting Started](../../getting-started/index.md) guide.
|
||||
|
||||
## Adding the Kubernetes frontend plugin
|
||||
|
||||
The first step is to add the Kubernetes frontend plugin to your Backstage
|
||||
application.
|
||||
The first step is to add the Kubernetes frontend plugin to your Backstage application.
|
||||
|
||||
```bash title="From your Backstage root directory"
|
||||
yarn --cwd packages/app add @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.
|
||||
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 title="packages/app/src/components/catalog/EntityPage.tsx"
|
||||
/* highlight-add-next-line */
|
||||
@@ -40,73 +37,17 @@ const serviceEntityPage = (
|
||||
);
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
:::note Note
|
||||
|
||||
- The optional `refreshIntervalMs` property on the `EntityKubernetesContent` defines the interval in which the content automatically refreshes, if not set this will default to 10 seconds.
|
||||
The optional `refreshIntervalMs` property on the `EntityKubernetesContent` defines the interval in which the content automatically refreshes, if not set this will default to 10 seconds.
|
||||
|
||||
That's it! But now, we need the Kubernetes Backend plugin for the frontend to
|
||||
work.
|
||||
:::
|
||||
|
||||
That's it! But now, we need the Kubernetes Backend plugin for the frontend to work.
|
||||
|
||||
## Adding Kubernetes Backend plugin
|
||||
|
||||
Navigate to `packages/backend` of your Backstage app, and install the
|
||||
`@backstage/plugin-kubernetes-backend` package.
|
||||
|
||||
```bash title="From your Backstage root directory"
|
||||
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend
|
||||
```
|
||||
|
||||
Create a file called `kubernetes.ts` inside `packages/backend/src/plugins/` and
|
||||
add the following:
|
||||
|
||||
```ts title="packages/backend/src/plugins/kubernetes.ts"
|
||||
import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const catalogApi = new CatalogClient({ discoveryApi: env.discovery });
|
||||
const { router } = await KubernetesBuilder.createBuilder({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
catalogApi,
|
||||
discovery: env.discovery,
|
||||
permissions: env.permissions,
|
||||
}).build();
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
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 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));
|
||||
}
|
||||
```
|
||||
|
||||
That's it! The Kubernetes frontend and backend have now been added to your
|
||||
Backstage app.
|
||||
|
||||
### New Backend System
|
||||
|
||||
To get the Kubernetes plugin install using the New Backend System you will need to do the following:
|
||||
|
||||
Run this command to add the package:
|
||||
First, we need to add the backend package:
|
||||
|
||||
```bash title="From your Backstage root directory"
|
||||
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend
|
||||
@@ -126,6 +67,9 @@ backend.add(import('@backstage/plugin-kubernetes-backend'));
|
||||
backend.start();
|
||||
```
|
||||
|
||||
That's it! The Kubernetes frontend and backend have now been added to your
|
||||
Backstage app.
|
||||
|
||||
### Custom cluster discovery
|
||||
|
||||
If either existing
|
||||
@@ -133,22 +77,18 @@ If either existing
|
||||
don't work for your use-case, it is possible to implement a custom
|
||||
[KubernetesClustersSupplier](https://backstage.io/docs/reference/plugin-kubernetes-backend.kubernetesclusterssupplier).
|
||||
|
||||
Change the following in `packages/backend/src/plugins/kubernetes.ts`:
|
||||
Here's a very simplified example:
|
||||
|
||||
```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 */
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { Duration } from 'luxon';
|
||||
import {
|
||||
ClusterDetails,
|
||||
KubernetesClustersSupplier,
|
||||
kubernetesClusterSupplierExtensionPoint,
|
||||
} from '@backstage/plugin-kubernetes-node';
|
||||
|
||||
/* highlight-add-start */
|
||||
export class CustomClustersSupplier implements KubernetesClustersSupplier {
|
||||
constructor(private clusterDetails: ClusterDetails[] = []) {}
|
||||
|
||||
@@ -170,43 +110,6 @@ export class CustomClustersSupplier implements KubernetesClustersSupplier {
|
||||
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,
|
||||
/* 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;
|
||||
}
|
||||
```
|
||||
|
||||
### New Backend System Custom cluster discovery
|
||||
|
||||
To use Custom cluster discovery with the New Backend System you'll need to create a module and add it to your backend. Here's a very simplified example:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { Duration } from 'luxon';
|
||||
import { kubernetesClusterSupplierExtensionPoint } from '@backstage/plugin-kubernetes-node';
|
||||
import { CustomClustersSupplier } from './path/to/class';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
@@ -236,7 +139,7 @@ backend.start();
|
||||
|
||||
:::note Note
|
||||
|
||||
This example assumes the `CustomClustersSupplier` class is the same from the [previous example](#custom-cluster-discovery)
|
||||
This example uses items from the `@backstage/plugin-kubernetes-node` and `luxon` packages, you'll need to add those for this example to work as is.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Initial support for pagination of the `CatalogIndexPage` was added in v1.21.0 of
|
||||
|
||||
## Initially Selected Filter
|
||||
|
||||
By default the initially selected filter defaults to Owned. If you are still building up your catalog this may show an empty list to start. If you would prefer this to show All as the default, here's how you can make that change:
|
||||
By default, the initially selected filter defaults to Owned. If you are still building up your catalog this may show an empty list to start. If you would prefer this to show All as the default, here's how you can make that change:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
<Route
|
||||
@@ -30,7 +30,7 @@ Possible options are: owned, starred, or all
|
||||
|
||||
## Initially Selected Kind
|
||||
|
||||
By default the initially selected Kind when viewing the Catalog is Component, but you may have reasons that you want this to be different. Let's say at your Organization they would like it to always default to Domain, here's how you would do that:
|
||||
By default, the initially selected Kind when viewing the Catalog is Component, but you may have reasons that you want this to be different. Let's say at your Organization they would like it to always default to Domain, here's how you would do that:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
<Route path="/catalog" element={<CatalogIndexPage initialKind="domain" />} />
|
||||
@@ -70,9 +70,7 @@ The columns you see in the `CatalogIndexPage` were selected to be a good startin
|
||||
Suppose we want to add a new User Email column to the `User` kind in the Catalog. We can do this by overriding the `columns` that we pass into the `CatalogIndexPage` component in our `App.tsx`. First, we need to match the entity kind that we want to override, and then define the columns to show:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
{
|
||||
/* highlight-add-start */
|
||||
}
|
||||
{/* prettier-ignore */ /* highlight-add-start */}
|
||||
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
|
||||
if (entityListContext.filters.kind?.value === 'user') {
|
||||
return [
|
||||
@@ -84,9 +82,7 @@ const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
|
||||
|
||||
return CatalogTable.defaultColumnsFunc(entityListContext);
|
||||
};
|
||||
{
|
||||
/* highlight-add-end */
|
||||
}
|
||||
{/* prettier-ignore */ /* highlight-add-end */}
|
||||
```
|
||||
|
||||
Then, we can implement the `createUserEmailColumn` function and add it to the list of columns. `field` is used to access the data from the entity, while `render` lets us customize how we display the data:
|
||||
@@ -107,7 +103,6 @@ const createUserEmailColumn = (): TableColumn<CatalogTableRow> => ({
|
||||
|
||||
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
|
||||
if (entityListContext.filters.kind?.value === 'user') {
|
||||
return [
|
||||
return [
|
||||
// Render existing columns
|
||||
...CatalogTable.defaultColumnsFunc(entityListContext),
|
||||
@@ -391,13 +386,9 @@ export const EntitySecurityTierPicker = () => {
|
||||
Now we can add the component to `CatalogIndexPage`:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
{
|
||||
/* highlight-add-start */
|
||||
}
|
||||
{/* prettier-ignore */ /* highlight-add-start */}
|
||||
import { DefaultFilters } from '@backstage/plugin-catalog-react';
|
||||
{
|
||||
/* highlight-add-end */
|
||||
}
|
||||
{/* prettier-ignore */ /* highlight-add-end */}
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
|
||||
@@ -495,8 +495,8 @@ want to have an isomorphic package that houses these types. Within the Backstage
|
||||
main repo the package naming pattern of `<plugin>-common` is used for isomorphic
|
||||
packages, and you may choose to adopt this pattern as well.
|
||||
|
||||
You can generate an isomorphic plugin package by running:`yarn new --select plugin-common`
|
||||
or you can run `yarn new` and then select "plugin-common" from the list of options
|
||||
You can generate an isomorphic plugin package by running: `yarn new` and then
|
||||
select "plugin-common" from the list of options
|
||||
|
||||
There's at this point no existing templates for generating isomorphic plugins
|
||||
using the `@backstage/cli`. Perhaps the simplest way to get started right now is
|
||||
|
||||
@@ -495,8 +495,8 @@ want to have an isomorphic package that houses these types. Within the Backstage
|
||||
main repo the package naming pattern of `<plugin>-common` is used for isomorphic
|
||||
packages, and you may choose to adopt this pattern as well.
|
||||
|
||||
You can generate an isomorphic plugin package by running:`yarn new --select plugin-common`
|
||||
or you can run `yarn new` and then select "plugin-common" from the list of options
|
||||
You can generate an isomorphic plugin package by running: `yarn new` and then
|
||||
selecting "plugin-common" from the list of options
|
||||
|
||||
There's at this point no existing templates for generating isomorphic plugins
|
||||
using the `@backstage/cli`. Perhaps the simplest way to get started right now is
|
||||
@@ -520,7 +520,7 @@ validate entities of our new kind. Just like with the definition package, you
|
||||
can find inspiration in for example the existing
|
||||
[ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-scaffolder-entity-model/src/processor/ScaffolderEntitiesProcessor.ts).
|
||||
|
||||
The custom processor should be created as a separate module for the catalog plugin. For information on how to set that up, see the [plugin docs](../../plugins/backend-plugin.md#creating-a-backend-plugin). Use `yarn new --select backend-module` instead to create a module. For our case, the module ID will be `foobar` and the plugin ID will be `catalog`.
|
||||
The custom processor should be created as a separate module for the catalog plugin. For information on how to set that up, see the [plugin docs](../../plugins/backend-plugin.md#creating-a-backend-plugin). Use `yarn new` and select `backend-module` instead to create a module. For our case, the module ID will be `foobar` and the plugin ID will be `catalog`.
|
||||
|
||||
We also provide a high-level example of what a catalog process for a custom
|
||||
entity might look like:
|
||||
|
||||
@@ -72,7 +72,7 @@ putting all extensions like this in a backend module package of their own in the
|
||||
`plugins` folder of your Backstage repo:
|
||||
|
||||
```sh
|
||||
yarn new --select backend-module --option id=catalog
|
||||
yarn new --select backend-module --option pluginId=catalog
|
||||
```
|
||||
|
||||
The class will have this basic structure:
|
||||
@@ -650,7 +650,7 @@ putting all extensions like this in a backend module package of their own in the
|
||||
`plugins` folder of your Backstage repo:
|
||||
|
||||
```sh
|
||||
yarn new --select backend-module --option id=catalog
|
||||
yarn new --select backend-module --option pluginId=catalog
|
||||
```
|
||||
|
||||
The class will have this basic structure:
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
---
|
||||
id: template-extensions
|
||||
title: Template Extensions
|
||||
description: Template extensions system
|
||||
---
|
||||
|
||||
Backstage templating is powered by [Nunjucks][]. The basics:
|
||||
|
||||
# Template Filters
|
||||
|
||||
The [filter][] is a critical mechanism for the rendering of Nunjucks templates,
|
||||
providing a means of transforming values in a familiar [piped][] fashion.
|
||||
Template filters are functions that help you transform data, extract specific
|
||||
information, and perform various operations in Scaffolder Templates.
|
||||
|
||||
## Built-in
|
||||
|
||||
Backstage provides out of the box the following set of "built-in" template
|
||||
filters (to create your own custom filters, look to the section [Custom Filter](#custom-filter) hereafter):
|
||||
|
||||
### parseRepoUrl
|
||||
|
||||
The `parseRepoUrl` filter parses a repository URL into its constituent parts:
|
||||
`owner`, repository name (`repo`), etc.
|
||||
|
||||
**Usage Example:**
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Parse Repo URL
|
||||
action: debug:log
|
||||
input:
|
||||
message: ${{ parameters.repoUrl | parseRepoUrl }}
|
||||
```
|
||||
|
||||
- **Input**: `github.com?repo=backstage&owner=backstage`
|
||||
- **Output**: "RepoSpec" (see [parseRepoUrl][])
|
||||
|
||||
### parseEntityRef
|
||||
|
||||
The `parseEntityRef` filter allows you to extract different parts of
|
||||
an entity reference, such as the `kind`, `namespace`, and `name`.
|
||||
|
||||
**Usage example**
|
||||
|
||||
1. Without context
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Parse Entity Reference
|
||||
action: debug:log
|
||||
input:
|
||||
message: ${{ parameters.owner | parseEntityRef }}
|
||||
```
|
||||
|
||||
- **Input**: `group:techdocs`
|
||||
- **Output**: [CompoundEntityRef][]
|
||||
|
||||
1. With context
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Parse Entity Reference
|
||||
action: debug:log
|
||||
input:
|
||||
message: ${{ parameters.owner | parseEntityRef({ defaultKind:"group", defaultNamespace:"another-namespace" }) }}
|
||||
```
|
||||
|
||||
- **Input**: `techdocs`
|
||||
- **Output**: [CompoundEntityRef][]
|
||||
|
||||
### pick
|
||||
|
||||
The `pick` filter allows you to select a specific property (e.g. `kind`, `namespace`, `name`) from an object.
|
||||
|
||||
**Usage Example**
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Pick
|
||||
action: debug:log
|
||||
input:
|
||||
message: ${{ parameters.owner | parseEntityRef | pick('name') }}
|
||||
```
|
||||
|
||||
- **Input**: `{ kind: 'Group', namespace: 'default', name: 'techdocs' }`
|
||||
- **Output**: `techdocs`
|
||||
|
||||
### projectSlug
|
||||
|
||||
The `projectSlug` filter generates a project slug from a repository URL.
|
||||
|
||||
**Usage Example**
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Project Slug
|
||||
action: debug:log
|
||||
input:
|
||||
message: ${{ parameters.repoUrl | projectSlug }}
|
||||
```
|
||||
|
||||
- **Input**: `github.com?repo=backstage&owner=backstage`
|
||||
- **Output**: `backstage/backstage`
|
||||
|
||||
# Template Globals
|
||||
|
||||
In addition to its powerful filtering functionality, the Nunjucks engine allows
|
||||
access from the template expression context to specified globally-accessible
|
||||
references. Backstage propagates this capability via the scaffolder backend
|
||||
plugin, which we shall soon see in action.
|
||||
|
||||
# Customizing the templating environment
|
||||
|
||||
Custom plugins make it possible to install your own template extensions, which
|
||||
may be any combination of filters, global functions and global values. With the
|
||||
new backend you would use a scaffolder plugin module for this; later we will
|
||||
demonstrate the analogous approach with the old backend.
|
||||
|
||||
## Streamlining Template Extension Module Creation with the Backstage CLI
|
||||
|
||||
The creation of a "template environment customization" module in Backstage can
|
||||
be accelerated using the Backstage CLI.
|
||||
|
||||
Start by using the `yarn backstage-cli new` command to generate a scaffolder module. This command sets up the necessary boilerplate code, providing a smooth start:
|
||||
|
||||
```
|
||||
$ yarn backstage-cli new
|
||||
? What do you want to create?
|
||||
> backend-module - A new backend module that extends an existing backend plugin with additional features
|
||||
backend-plugin - A new backend plugin
|
||||
plugin - A new frontend plugin
|
||||
node-library - A new node-library package, exporting shared functionality for backend plugins and modules
|
||||
plugin-common - A new isomorphic common plugin package
|
||||
plugin-node - A new Node.js library plugin package
|
||||
plugin-react - A new web library plugin package
|
||||
scaffolder-module - An module exporting custom actions for @backstage/plugin-scaffolder-backend
|
||||
```
|
||||
|
||||
When prompted, select the option to generate a backend module.
|
||||
Since we want to extend the Scaffolder backend, enter `scaffolder` when prompted for the plugin to extend.
|
||||
Next, enter a name for your module (relative to the generated `scaffolder-backend-module-` prefix),
|
||||
and the CLI will generate the required files and directory structure.
|
||||
|
||||
## Writing your Module
|
||||
|
||||
Once the CLI has generated the essential structure for your new scaffolder
|
||||
module, it's time to implement our template extensions. Here we'll demonstrate
|
||||
how to create each of the supported extension types.
|
||||
|
||||
`src/module.ts` is where the magic happens. First we prepare to utilize the
|
||||
associated (_**alpha** phase_) API extension point by adding:
|
||||
|
||||
```ts
|
||||
import { scaffolderTemplatingExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
|
||||
```
|
||||
|
||||
Considering the generated code, you may observe that everything rests on the
|
||||
`createBackendModule` call, which after providing some minimal metadata to
|
||||
establish context, specifies a `register` callback whose sole responsibility
|
||||
here is to call, in turn, `registerInit` against the
|
||||
`BackendModuleRegistrationPoints` argument it receives. Modify this call to
|
||||
make the `scaffolderTemplatingExtensionPoint` available to the specified `init`
|
||||
function:
|
||||
|
||||
```ts
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
...,
|
||||
templating: scaffolderTemplatingExtensionPoint,
|
||||
},
|
||||
async init({
|
||||
...,
|
||||
templating
|
||||
}) {
|
||||
...
|
||||
};
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Now we're ready to extend the scaffolder templating engine. For our purposes
|
||||
here we'll drop everything in `module.ts`; use your own judgment as to the
|
||||
organization of your real-world plugin modules.
|
||||
|
||||
### Custom Filter
|
||||
|
||||
In this contrived example we add a filter to test whether the incoming string
|
||||
value contains (at least) a specified number of occurrences of a given
|
||||
substring. We can easily define this by adding code to our `init` callback:
|
||||
|
||||
```ts
|
||||
async init({
|
||||
...,
|
||||
templating,
|
||||
}) {
|
||||
...
|
||||
templating.addTemplateFilters({
|
||||
containsOccurrences: (arg: string, substring: string, times: number) => {
|
||||
let pos = 0;
|
||||
let count = 0;
|
||||
while (pos < arg.length) {
|
||||
pos = arg.indexOf(substring, pos);
|
||||
if (pos < 0) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return count === times;
|
||||
},
|
||||
});
|
||||
},
|
||||
```
|
||||
|
||||
This demonstrates the bare minimum: a TypeScript `Record` of named template
|
||||
filter implementations to register. However, by adopting an alternate structure
|
||||
we can document our filter with additional metadata; to utilize this capability
|
||||
we begin by adding a new import:
|
||||
|
||||
```ts
|
||||
import { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
|
||||
```
|
||||
|
||||
Then, update your `init` implementation to specify an array rather than an
|
||||
object/record:
|
||||
|
||||
```ts
|
||||
async init({
|
||||
...,
|
||||
templating,
|
||||
}) {
|
||||
...
|
||||
templating.addTemplateFilters([
|
||||
createTemplateFilter({
|
||||
id: 'containsOccurrences',
|
||||
description: 'determine whether filter input contains a substring N times',
|
||||
filter: (arg: string, substring: string, times: number) => {
|
||||
let pos = 0;
|
||||
let count = 0;
|
||||
while (pos < arg.length) {
|
||||
pos = arg.indexOf(substring, pos);
|
||||
if (pos < 0) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return count === times;
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
```
|
||||
|
||||
With this we have added a `description` to our filter, which helps a template
|
||||
author to understand the filter's purpose.
|
||||
|
||||
#### Schema
|
||||
|
||||
To enhance our filter documentation further, we will specify its `schema`
|
||||
using a callback against the [Zod][] schema declaration library:
|
||||
|
||||
```ts
|
||||
createTemplateFilter({
|
||||
id: 'containsOccurrences',
|
||||
description: 'determine whether filter input contains a substring N times',
|
||||
schema: z =>
|
||||
z.function(
|
||||
z.tuple([
|
||||
z.string().describe('input'),
|
||||
z.string().describe('substring whose occurrences to find'),
|
||||
z.number().describe('number of occurrences to check for'),
|
||||
]),
|
||||
z.boolean(),
|
||||
),
|
||||
...,
|
||||
}),
|
||||
```
|
||||
|
||||
Because a filter is, in fact, a function, its schema is defined by generating a
|
||||
[Zod function schema][zod-fn] against the parameter supplied to our schema
|
||||
callback. A filter function is required to have at least one argument; in this
|
||||
example, we have two additional arguments. But what if we modify our filter's
|
||||
implementation function to make `times` optional? Code:
|
||||
|
||||
```ts
|
||||
createTemplateFilter({
|
||||
id: 'containsOccurrences',
|
||||
...,
|
||||
filter: (arg: string, substring: string, times?: number) => {
|
||||
if (times === undefined) {
|
||||
// note that, in real life, simply calling this function directly with Nunjucks would suffice rather than implementing a filter:
|
||||
return arg.includes(substring);
|
||||
}
|
||||
// original implementation follows
|
||||
...
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
In this case we should modify our `schema`:
|
||||
|
||||
```ts
|
||||
createTemplateFilter({
|
||||
...,
|
||||
schema: z =>
|
||||
z.function(
|
||||
z.tuple([
|
||||
z.string().describe('input'),
|
||||
z.string().describe('substring whose occurrences to find'),
|
||||
z
|
||||
.number()
|
||||
.describe('number of occurrences to check for')
|
||||
.optional(),
|
||||
]),
|
||||
z.boolean(),
|
||||
),
|
||||
...,
|
||||
}),
|
||||
```
|
||||
|
||||
#### Filter Example Documentation
|
||||
|
||||
Our filter documentation may benefit from examples which we specify thus:
|
||||
|
||||
```ts
|
||||
createTemplateFilter({
|
||||
...,
|
||||
examples: [
|
||||
{
|
||||
description: 'Basic Usage',
|
||||
example: `\
|
||||
- name: Contains Occurrences
|
||||
action: debug:log
|
||||
input:
|
||||
message: \${{ parameters.projectName | containsOccurrences('-', 2) }}
|
||||
`,
|
||||
notes: `\
|
||||
- **Input**: \`foo-bar-baz\`
|
||||
- **Output**: \`true\`
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: 'Omitting Optional Parameter',
|
||||
example: `\
|
||||
- name: Contains baz
|
||||
action: debug:log
|
||||
input:
|
||||
message: \${{ parameters.projectName | containsOccurrences('baz') | dump }}
|
||||
`,
|
||||
notes: `\
|
||||
- **Input**: \`foo-bar\`
|
||||
- **Output**: \`false\`
|
||||
`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
```
|
||||
|
||||
### Custom Global Function
|
||||
|
||||
In case your template needs access to a value generated from a function not
|
||||
appropriately modeled as a filter, Nunjucks supports the direct invocation of
|
||||
[global functions][global-fn]. We might, for example, add to `init`:
|
||||
|
||||
```ts
|
||||
async init({
|
||||
...,
|
||||
templating,
|
||||
}) {
|
||||
...
|
||||
templating.addTemplateGlobals({
|
||||
now: () => new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
```
|
||||
|
||||
Here we have implemented a simple mechanism to obtain a timestamp (note that
|
||||
because we can only pass JSON-compatible--or `undefined`--values we have chosen
|
||||
to model a date/time as an ISO string) using a globally available function.
|
||||
|
||||
Again we have the option to make our global function self-documenting. Import:
|
||||
|
||||
```ts
|
||||
import {
|
||||
...,
|
||||
createTemplateGlobalFunction,
|
||||
} from '@backstage/plugin-scaffolder-node/alpha';
|
||||
```
|
||||
|
||||
Then modify:
|
||||
|
||||
```ts
|
||||
...
|
||||
templating.addTemplateGlobals([
|
||||
createTemplateGlobalFunction({
|
||||
id: 'now',
|
||||
description:
|
||||
'obtain an ISO representation of the current date and time',
|
||||
fn: () => new Date().toISOString(),
|
||||
}),
|
||||
]);
|
||||
```
|
||||
|
||||
#### Schema
|
||||
|
||||
Declaring a global function schema is quite like the schema declaration for a
|
||||
template filter:
|
||||
|
||||
```ts
|
||||
createTemplateGlobal({
|
||||
...,
|
||||
schema: z => z.function().args().returns(z.string()),
|
||||
...,
|
||||
}),
|
||||
```
|
||||
|
||||
#### Template Global Function Example Documentation
|
||||
|
||||
Again, this works in the same way as filter examples:
|
||||
|
||||
```ts
|
||||
createTemplateGlobal({
|
||||
...,
|
||||
examples: [
|
||||
{
|
||||
description: 'Obtain the current date/time',
|
||||
example: `\
|
||||
- name: Log Timestamp
|
||||
action: debug:log
|
||||
input:
|
||||
message: Current date/time: \${{ now() }}
|
||||
`,
|
||||
// optional `notes` omitted from this example
|
||||
},
|
||||
],
|
||||
...,
|
||||
}),
|
||||
|
||||
```
|
||||
|
||||
### Custom Global Value
|
||||
|
||||
Alternatively, your template may need access to a simple JSON value, which can
|
||||
be registered in this manner:
|
||||
|
||||
```ts
|
||||
async init({
|
||||
...,
|
||||
templating,
|
||||
}) {
|
||||
...
|
||||
templating.addTemplateGlobals({
|
||||
...,
|
||||
preferredMetasyntacticIdentifier: 'foo',
|
||||
});
|
||||
},
|
||||
```
|
||||
|
||||
Or the documenting form:
|
||||
|
||||
```ts
|
||||
async init({
|
||||
...,
|
||||
templating,
|
||||
}) {
|
||||
...
|
||||
templating.addTemplateGlobals([
|
||||
...,
|
||||
createTemplateGlobalValue({
|
||||
id: 'preferredMetasyntacticVariable',
|
||||
value: 'foo',
|
||||
description:
|
||||
'This description is as contrived as the global value it documents',
|
||||
}),
|
||||
]);
|
||||
},
|
||||
```
|
||||
|
||||
## Register Template Extensions with the Legacy Backend System
|
||||
|
||||
Users of the original Backstage backend can register template extensions by
|
||||
specifying options to the scaffolder backend plugin's `createRouter` function
|
||||
(customarily called in `packages/backend/src/plugins/scaffolder.ts`):
|
||||
|
||||
- `additionalTemplateFilters` - either of:
|
||||
- object mapping filter name to implementation function, or
|
||||
- array of documented template filters as returned by the
|
||||
utility function `createTemplateFilter`
|
||||
- `additionalTemplateGlobals` - either of:
|
||||
- object mapping global name to value or function, or
|
||||
- array of documented global functions and values as returned by the utility
|
||||
functions `createTemplateGlobalFunction` and `createTemplateGlobalValue`
|
||||
|
||||
[nunjucks]: https://mozilla.github.io/nunjucks
|
||||
[filter]: https://mozilla.github.io/nunjucks/templating.html#filters
|
||||
[global-fn]: https://mozilla.github.io/nunjucks/templating.html#global-functions
|
||||
[parseRepoUrl]: https://backstage.io/docs/reference/plugin-scaffolder-node.parserepourl
|
||||
[CompoundEntityRef]: https://backstage.io/docs/reference/catalog-model.compoundentityref
|
||||
[Zod]: https://zod.dev/
|
||||
[zod-fn]: https://zod.dev/?id=functions
|
||||
[piped]: https://en.wikipedia.org/wiki/Pipeline_(Unix)#Pipelines_in_command_line_interfaces
|
||||
@@ -631,7 +631,7 @@ output:
|
||||
|
||||
## The templating syntax
|
||||
|
||||
You might have noticed variables wrapped in `${{ }}` in the examples. These are
|
||||
You might have noticed expressions wrapped in `${{ }}` in the examples. These are
|
||||
template strings for linking and gluing the different parts of the template
|
||||
together. All the form inputs from the `parameters` section will be available by
|
||||
using this template syntax (for example, `${{ parameters.firstName }}` inserts
|
||||
@@ -704,219 +704,16 @@ You can read more about all the `inputs` and `outputs` defined in the actions in
|
||||
code part of the `JSONSchema`, or you can read more about our
|
||||
[built in actions](./builtin-actions.md).
|
||||
|
||||
## Built in Filters
|
||||
### More about expressions
|
||||
|
||||
Template filters are functions that help you transform data, extract specific information,
|
||||
and perform various operations in Scaffolder Templates.
|
||||
The `${{ }}` constructs in your template are evaluated using the
|
||||
powerful [Nunjucks templating engine](https://mozilla.github.io/nunjucks/).
|
||||
To learn more about basic Nunjucks templating please see
|
||||
[templating documentation](https://mozilla.github.io/nunjucks/templating.html).
|
||||
|
||||
This section introduces the built-in filters provided by Backstage and offers examples of
|
||||
how to use them in the Scaffolder templates. It's important to mention that Backstage also leverages the
|
||||
native filters from the Nunjucks library. For a complete list of these native filters and their usage,
|
||||
refer to the [Nunjucks documentation](https://mozilla.github.io/nunjucks/templating.html#builtin-filters).
|
||||
|
||||
To create your own custom filters, look to the section [Custom Filters and Globals](#custom-filters-and-globals) hereafter.
|
||||
|
||||
### parseRepoUrl
|
||||
|
||||
The `parseRepoUrl` filter parse a repository URL into
|
||||
its components, such as `owner`, repository `name`, and more.
|
||||
|
||||
**Usage Example:**
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Parse Repo URL
|
||||
action: debug:log
|
||||
input:
|
||||
extra: ${{ parameters.repoUrl | parseRepoUrl }}
|
||||
```
|
||||
|
||||
- **Input**: `github.com?repo=backstage&org=backstage`
|
||||
- **Output**: [RepoSpec](https://github.com/backstage/backstage/blob/v1.17.2/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts#L39)
|
||||
|
||||
### parseEntityRef
|
||||
|
||||
The `parseEntityRef` filter allows you to extract different parts of
|
||||
an entity reference, such as the `kind`, `namespace`, and `name`.
|
||||
|
||||
**Usage example**
|
||||
|
||||
1. Without context
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Parse Entity Reference
|
||||
action: debug:log
|
||||
input:
|
||||
extra: ${{ parameters.owner | parseEntityRef }}
|
||||
```
|
||||
|
||||
- **Input**: `group:techdocs`
|
||||
- **Output**: [CompoundEntityRef](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/types.ts#L23)
|
||||
|
||||
2. With context
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Parse Entity Reference
|
||||
action: debug:log
|
||||
input:
|
||||
extra: ${{ parameters.owner | parseEntityRef({ defaultKind:"group", defaultNamespace:"another-namespace" }) }}
|
||||
```
|
||||
|
||||
- **Input**: `techdocs`
|
||||
- **Output**: [CompoundEntityRef](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/types.ts#L23)
|
||||
|
||||
### pick
|
||||
|
||||
This `pick` filter allows you to select specific properties (`kind`, `namespace`, `name`) from an object.
|
||||
|
||||
**Usage Example**
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Pick
|
||||
action: debug:log
|
||||
input:
|
||||
extra: ${{ parameters.owner | parseEntityRef | pick('name') }}
|
||||
```
|
||||
|
||||
- **Input**: `{ kind: 'Group', namespace: 'default', name: 'techdocs' }`
|
||||
- **Output**: `techdocs`
|
||||
|
||||
### projectSlug
|
||||
|
||||
The `projectSlug` filter generates a project slug from a repository URL
|
||||
|
||||
**Usage Example**
|
||||
|
||||
```yaml
|
||||
- id: log
|
||||
name: Project Slug
|
||||
action: debug:log
|
||||
input:
|
||||
extra: ${{ parameters.repoUrl | projectSlug }}
|
||||
```
|
||||
|
||||
- **Input**: `github.com?repo=backstage&org=backstage`
|
||||
- **Output**: `backstage/backstage`
|
||||
|
||||
## Custom Filters and Globals
|
||||
|
||||
You may wish to extend the filters and globals with your own custom ones. For example `${{ myGlobal | myFilter | myOtherFilter }}` or `${{ myFunctionGlobal(1,2) | myFilter }}`.
|
||||
This can be achieved using the `additionalTemplateFilters` and `additionalTemplateGlobals` properties respectively.
|
||||
|
||||
These properties accept a `Record`
|
||||
|
||||
```ts title="plugins/scaffolder-backend/src/service/router.ts"
|
||||
additionalTemplateFilters?: Record<string, TemplateFilter>;
|
||||
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
|
||||
```
|
||||
|
||||
where the first parameter is the identifier of the filter or global and the second is a `TemplateFilter` or a `TemplateGlobal` respectively.
|
||||
A `TemplateFilter` is a function which will be called using the previous `JsonValue` objects and may return a `JsonValue` object.
|
||||
A `TemplateGlobal` can either be a function which will be called using the passed `JsonValue` objects and may return a `JsonValue` object or it can be a `JsonValue` object itself.
|
||||
|
||||
```ts title="plugins/scaffolder-node/src/types.ts"
|
||||
export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined;
|
||||
|
||||
export type TemplateGlobal =
|
||||
| ((...args: JsonValue[]) => JsonValue | undefined)
|
||||
| JsonValue;
|
||||
```
|
||||
|
||||
**Usage Example**
|
||||
|
||||
Given you want to have the following filters and globals available in you template:
|
||||
|
||||
```yaml
|
||||
apiVersion: scaffolder.backstage.io/v1beta3
|
||||
kind: Template
|
||||
metadata:
|
||||
name: test
|
||||
title: Test
|
||||
spec:
|
||||
owner: user:guest
|
||||
type: service
|
||||
|
||||
steps:
|
||||
- id: debug1
|
||||
name: debug1
|
||||
action: debug:log
|
||||
input:
|
||||
message: ${{ myGlobal | myFilter | myOtherFilter }}
|
||||
|
||||
- id: debug2
|
||||
name: debug2
|
||||
action: debug:log
|
||||
input:
|
||||
message: ${{ myFunctionGlobal(1,2) | myFilter }}
|
||||
```
|
||||
|
||||
You will have to create a new [`BackendModule`](../../backend-system/architecture/06-modules.md) using the `scaffolderTemplatingExtensionPoint`.
|
||||
|
||||
Here is a very simplified example of how to do that:
|
||||
|
||||
```ts title="packages/backend-next/src/index.ts"
|
||||
/* highlight-add-start */
|
||||
import { scaffolderTemplatingExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
/* highlight-add-end */
|
||||
|
||||
/* highlight-add-start */
|
||||
const scaffolderModuleCustomFilters = createBackendModule({
|
||||
pluginId: 'scaffolder', // name of the plugin that the module is targeting
|
||||
moduleId: 'custom-filters',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
scaffolder: scaffolderTemplatingExtensionPoint,
|
||||
// ... and other dependencies as needed
|
||||
},
|
||||
async init({ scaffolder /* ..., other dependencies */ }) {
|
||||
scaffolder.addTemplateGlobals({
|
||||
myGlobal: () => 'myGlobal',
|
||||
myFunctionGlobal: (...args: JsonValue[]) => args[0] + args[1],
|
||||
});
|
||||
scaffolder.addTemplateFilters({
|
||||
myFilter: () => 'the value is this now',
|
||||
myOtherFilter: (...args: JsonValue[]) => args.join(''),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(scaffolderModuleCustomFilters);
|
||||
```
|
||||
|
||||
If you still use the legacy backend system, then you will use the `createRouter()` function of the `Scaffolder plugin`
|
||||
|
||||
```ts title="packages/backend/src/plugins/scaffolder.ts"
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
...
|
||||
return await createRouter({
|
||||
logger,
|
||||
config,
|
||||
|
||||
additionalTemplateFilters: {
|
||||
<YOUR_FILTERS>
|
||||
},
|
||||
additionalTemplateGlobals: {
|
||||
<YOUR_GLOBALS>
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Note that additional template global functions are currently not supported in `fetch:template` (see #25445).
|
||||
Information about Backstage's built-in Nunjucks extensions, as well as how to
|
||||
create your own customizations, may be found at
|
||||
[Template Extensions](./template-extensions.md).
|
||||
|
||||
## Template Editor
|
||||
|
||||
|
||||
Reference in New Issue
Block a user