Skip to main content

Experimental Features

Introduction

This section contains information and guides on the experimental features that are currently available in the Scaffolder. Be advised that these features are still in development and may not be fully stable or complete, and are subject to change at any time.

Please leave feedback on these features in the Backstage Discord or by creating an issue on the Backstage GitHub repository.

Retries and Recovery

Running tasks, especially longer running ones can be at risk of being lost when the scaffolder-backend plugin is redeployed. These tasks will just be stuck in a processing state, with no real way to recover them.

The experimental Retries and Recovery is here to help mitigate this.

Whenever you do redeploy, on startup there will be a check of all tasks in processing state that you identified in your template as being capable of starting over.

More details about the motivation and the goals of this feature can be found in the Scaffolder Retries and Idempotency BEP

Here is an example of how you can enable this in your template.yaml manifest:

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: recoverable-template
spec:
EXPERIMENTAL_recovery:
EXPERIMENTAL_strategy: startOver

You'll also need enable the recovery feature, add this snippet into your app-config.yaml file:

scaffolder:
EXPERIMENTAL_recoverTasks: true

By default, the tasks that are in a processing state and have not reported back with a heartbeat for longer than 30 seconds will be automatically recovered.

This implies that the task's status will shift to open initiating and will be restarted from the beginning. This means that it's important that your actions that you have in the template run are idempotent.

You can look at how to incorporate checkpoints into your custom actions to achieve that.

In the case that you would like to make the heartbeat threshold shorter or longer than the default 30 seconds, you can customize it for your needs with the configuration:

scaffolder:
EXPERIMENTAL_recoverTasksTimeout: { minutes: 1 }

If your task works with the filesystem and stores files in the workspace and you want to store these workspaces across runs, you can enable this with some additional config to app-config.yaml

scaffolder:
EXPERIMENTAL_workspaceSerialization: true

By default, the serialized workspace will be stored in the database, however if there's larger files, or if you're worried about the size of these files taking up space in the database you can configure bucket storage and have a sensible retention policy there to cleanup older files.

At the moment we also support integration with Google GCS; to switch the serialization to this provider, you can do so with:

scaffolder:
EXPERIMENTAL_workspaceSerializationProvider: gcpBucket
EXPERIMENTAL_workspaceSerializationGcpBucketName: name-of-your-bucket

You don't need to provide any extra configuration, but you have to be sure that you are using workload identity.

Form Decorators

Form decorators provide the ability to run arbitrary code before the form is submitted along with secrets to the scaffolder-backend plugin. They are provided to the app using a Utility API.

Installation

To install the Form Decorators, add the following to your packages/app/src/apis.ts:

  createApiFactory({
api: formDecoratorsApiRef,
deps: {},
factory: () =>
DefaultScaffolderFormDecoratorsApi.create({
decorators: [
// add decorators here
],
}),
}),

And then you'll also need to define which decorators run in each template using the EXPERIMENTAL_formDecorators key in the template's spec:

kind: Template
metadata:
name: my-template
spec:
EXPERIMENTAL_formDecorators:
- id: my-decorator
input:
test: something funky

parameters: ...
steps: ...

Creating a Decorator

You can create a decorator using the simple helper method createScaffolderFormDecorator:

export const mockDecorator = createScaffolderFormDecorator({
// give the decorator a name
id: 'mock-decorator',

// define the schema for the input that can be proided in `template.yaml`
schema: {
input: {
test: z => z.string(),
},
},
deps: {
// define dependencies here
githubApi: githubAuthApiRef,
},
decorator: async (
// Context has all the things needed to write simple decorators
{ setSecrets, setFormState, input: { test } },
// Depepdencies injected here
{ githubApi },
) => {
// mutate the form state
setFormState(state => ({ ...state, test, mock: 'MOCK' }));

// mutate the form secrets
setSecrets(state => ({ ...state, GITHUB_TOKEN: 'MOCK_TOKEN' }));
},
});