Skip to main content

Build a plugin

Plugin use cases

Here are a few reference guides on common use cases for creating custom plugins in Portal.

Add a custom scaffolder action to software templates

Software templates allow your developers to start from square two when building a new application. Software templates have a series of actions – steps that run when using a template. For example, publishing to source control, deleting or renaming files, and registering software in the catalog.

Portal has many built-in actions, but as you explore software templates you may find that you need a custom action to perform a step that may be unique to your company. For example, at Spotify we have custom actions to migrate files from one format to another, or to link Google Cloud projects to a software component.

As an example, we’ll create a scaffolder module to add a custom action that pings an internal system with the ID of a new component. This might be used to kick off a security review, for instance.

Before you begin

Verify you’ve set up a Backstage application locally. See the getting started section for more information.

1. Create your plugin

Run the new command from the root of your Backstage application. For the first prompt after selecting the scaffolder-module type.

$ yarn backstage-cli new

> scaffolder-module - A module exporting custom actions for @backstage/plugin-scaffolder-backend

? Enter the name of the module [required]: pinger

This will create a plugins/scaffolder-backend-module-pinger folder with the structure of a backend module.

2. Modify the example action

The scaffolder-module plugin that was created comes with an example custom action. Let’s modify this example slightly to ping an external service with the entity reference that was created by a template that includes this action:

plugins/scaffolder-backend-module-pinger/src/actions/example.ts
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';

export function createPingAction() {
return createTemplateAction<{
entityRef: string;
}>({
id: 'acme:ping',
description: 'Pings an external service',
schema: {
input: {
type: 'object',
required: ['entityRef'],
properties: {
entityRef: {
title: 'An entity reference',
description:
"An entity reference, e.g. 'component:default/my-component'",
type: 'string',
},
},
},
},
async handler(ctx) {
ctx.logger.info(
`Pinging external service for entity: ${ctx.input.entityRef}`,
);

await fetch('https://httpbin.org/post', {
body: JSON.stringify({ entityRef: ctx.input.entityRef }),
method: 'POST',
});
},
});
}

Note that this action is using the sample httpbin service for demonstration purposes; typically the fetch URL here would be an internal service at your company.

3. Update the backend module

The backend module needs a small update, since we changed the name of the example action:

plugins/scaffolder-backend-module-pinger/src/module.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
- import { createExampleAction } from './actions/example';
+ import { createPingAction } from './actions/example';

/**
* A backend module that registers the action into the scaffolder
*/
export const scaffolderModule = createBackendModule({
moduleId: 'example-action',
pluginId: 'scaffolder',
register({ registerInit }) {
registerInit({
deps: {
scaffolderActions: scaffolderActionsExtensionPoint,
},
async init({ scaffolderActions }) {
- scaffolderActions.addActions(createExampleAction());
+ scaffolderActions.addActions(createPingAction());
},
});
},
});

4. Install in your Backstage application

Now we can add the plugin to the Backstage application. First, we need to add the dependency to the backend package.

Note: The backstage-cli new command may have already added the plugin as a dependency, so only add this line if needed.

packages/backend/package.json
{
"name": "backend",
"version": "0.0.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
"backstage": {
"role": "backend"
},
"scripts": {
...
},
"dependencies": {
...
+ "backstage-plugin-scaffolder-backend-module-pinger": "workspace:^",
}
...
}

Next, the plugin can be added to the backend initialization:

packages/backend/src/index.ts
...
backend.add(import('backstage-plugin-scaffolder-backend-module-pinger'));
backend.start();

This step of adding the module to the application is only needed in an open-source Backstage application. Portal detects added plugins and wires them up to the application automatically.

Run yarn dev to start up the application. Visit the scaffolder action list to verify that the new acme:ping action is listed.

Custom scaffolder action list showing ping action

5. Use the custom action in a software template

To put this action to use, we can modify the example template that comes with a Backstage application to use the new action. Let’s add the acme:ping action after the register step, since we want to use the entityRef that the register step outputs:

examples/templates/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: example-nodejs-template
title: Example Node.js Template
description: An example template for the scaffolder that creates a simple Node.js service
spec:
owner: user:guest
type: service

parameters:
...

steps:
- id: fetch-base
name: Fetch Base
action: fetch:template
input:
url: ./content
values:
name: ${{ parameters.name }}

# This step publishes the contents of the working directory to GitHub.
- id: publish
name: Publish
action: publish:github
input:
allowedHosts: ['github.com']
description: This is ${{ parameters.name }}
repoUrl: ${{ parameters.repoUrl }}

# The final step is to register our new component in the catalog.
- id: register
name: Register
action: catalog:register
input:
repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
catalogInfoPath: '/catalog-info.yaml'
+ - id: ping
+ name: Ping an external service
+ action: acme:ping
+ input:
+ entityRef: ${{ steps['register'].output.entityRef }}

# Outputs are displayed to the user after a successful execution of the template.
output:
...

Since the example template uses the publish:github action, you’ll also need to set up the GitHub integration to see the template work successfully. Follow the instructions to set up your app-config.yaml with a valid GitHub token.

Restart the application with yarn dev to ensure the template change is picked up. Go to Create in the navigation and select the example template. Fill out the form fields, click Review and then Create at the end.

The template will execute, showing the new acme:ping action as the last step in the template:

Example template run showing succesful ping step

Connect & sync software catalog to an external source

The software catalog is at the heart of Portal and Backstage. The software catalog is often populated with software components that have metadata descriptor files in source control, but it’s not unusual to pull in data from another source — such as an API gateway or another third-party service that tracks software at your company.

As an example, we’ll create a catalog module that loads API components from Apigee, an API gateway offered by Google Cloud.

Before you begin

Verify you’ve set up a Backstage application locally. See the getting started section for more information.

1. Create your plugin

Run the new command from the root of your Backstage application. For the first prompt after selecting the backend-module type, enter catalog for the plugin — this module will extend the catalog (which is itself a plugin!) to pull in data from an external source.

$ yarn backstage-cli new

# Select `backend-module` to create a backend module
> backend-module - A new backend module that extends an existing backend plugin with additional features

? Enter the ID of the plugin [required]: catalog
? Enter the ID of the module [required]: apigee

This will create a plugins/catalog-backend-module-apigee folder with the structure of a backend module.

2. Create a proxy endpoint

To call a third-party service, it’s common to create an endpoint in the Backstage proxy that your plugin can call; this way, authentication is handled in configuration.

app-config.yaml
proxy:
endpoints:
'/apigee':
# Note: replace <project> in the target with a Google Cloud project name
target: 'https://apigee.googleapis.com/v1/organizations/<project>/apis'
headers:
# APIGEE_TOKEN should be set as an environment variable; you can run the app
# using `APIGEE_TOKEN=xxx yarn dev` or hard-code the token here instead for
# testing purposes.
Authorization: Bearer ${APIGEE_TOKEN}

3. Create an entity provider

The catalog plugin supports adding entity providers, which can populate the software catalog outside of the normal method of a metadata file in source control.

Create a new source file for the entity provider called ApigeeEntityProvider.ts:

plugins/catalog-backend-module-apigee/src/ApigeeEntityProvider.ts
import fetch from 'node-fetch';
import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
Entity,
} from '@backstage/catalog-model';
import {
AuthService,
DiscoveryService,
SchedulerServiceTaskRunner,
} from '@backstage/backend-plugin-api';
import {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';

type ApigeeApiResponse = {
proxies: {
name: string;
apiProxyType: string;
}[];
};

export class ApigeeEntityProvider implements EntityProvider {
private auth: AuthService;
private connection?: EntityProviderConnection;
private discovery: DiscoveryService;
private taskRunner: SchedulerServiceTaskRunner;

constructor(
taskRunner: SchedulerServiceTaskRunner,
discovery: DiscoveryService,
auth: AuthService,
) {
this.taskRunner = taskRunner;
this.discovery = discovery;
this.auth = auth;
}

getProviderName(): string {
return 'apigee';
}

async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
await this.taskRunner.run({
id: this.getProviderName(),
fn: async () => {
await this.run();
},
});
}

async run(): Promise<void> {
if (!this.connection) {
throw new Error('Not initialized');
}

// Get a service auth token to call the proxy
const { token } = await this.auth.getPluginRequestToken({
onBehalfOf: await this.auth.getOwnServiceCredentials(),
targetPluginId: 'proxy',
});

const proxyUrl = await this.discovery.getBaseUrl('proxy');
const response = await fetch(`${proxyUrl}/apigee`, {
headers: { Authorization: `Bearer ${token}` },
});
const data = (await response.json()) as ApigeeApiResponse;

const entities: Entity[] = this.convertApiEntities(data);
await this.connection.applyMutation({
type: 'full',
entities: entities.map((entity) => ({
entity,
locationKey: `apigee-provider:${entity.metadata.name}`,
})),
});
}

convertApiEntities(data: ApigeeApiResponse): Entity[] {
return data.proxies.map((proxy) => ({
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: proxy.name,
annotations: {
[ANNOTATION_LOCATION]: `apigee://${proxy.name}`,
[ANNOTATION_ORIGIN_LOCATION]: `apigee://${proxy.name}`,
},
},
spec: {
type: 'apigee',
owner: 'platform-team',
lifecycle: 'production',
description: `Apigee API proxy of type ${proxy.apiProxyType}`,
definition: 'TODO', // TODO: load API definition
},
}));
}
}

This entity provider uses the proxy endpoint we created, along with Backstage service-to-service auth, to call the Apigee API and retrieve a list of API proxies to add to the catalog. API types in Backstage require a definition field, which this particular Apigee API doesn’t provide, so this is an exercise for later.

4. Register the entity provider in your plugin

Now that the entity provider is created, you can register it in the plugin module using the catalogProcessingExtensionPoint:

plugins/catalog-backend-module-apigee/src/module.ts
import {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { ApigeeEntityProvider } from './ApigeeEntityProvider';

export const catalogModuleApigee = createBackendModule({
pluginId: 'catalog',
moduleId: 'apigee',
register(env) {
env.registerInit({
deps: {
auth: coreServices.auth,
catalog: catalogProcessingExtensionPoint,
discovery: coreServices.discovery,
scheduler: coreServices.scheduler,
},
async init({ auth, catalog, discovery, scheduler }) {
const taskRunner = scheduler.createScheduledTaskRunner({
frequency: {
minutes: 1,
},
timeout: { minutes: 5 },
});
catalog.addEntityProvider(
new ApigeeEntityProvider(taskRunner, discovery, auth),
);
},
});
},
});

5. Install in your Backstage application

Now we have a backend module that registers an entity provider – the last step is to add the plugin to the Backstage application. First, we need to add the dependency to the backend package.

Note: The backstage-cli new command may have already added the plugin as a dependency, so only add this line if needed.

packages/backend/package.json
{
"name": "backend",
"version": "0.0.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
"backstage": {
"role": "backend"
},
"scripts": {
...
},
"dependencies": {
...
+ "backstage-plugin-catalog-backend-module-apigee": "workspace:^",
}
...
}

Next, the plugin can be added to the backend initialization:

packages/backend/src/index.ts
...
backend.add(import('backstage-plugin-catalog-backend-module-apigee'));
backend.start();

This final step of adding the module to the application is only needed in an open-source Backstage application. Portal detects added plugins and wires them up to the application automatically.

Run yarn dev to start up the application and verify that the API entities from Apigee appear in the catalog. The software catalog view defaults to Components, so make sure to switch to the API kind to see the new entities.