beps/0010-scaffolder-templating-in-parameters/README.md
parameters schemaThis BEP proposes to add support for templating syntax in the parameters schema of a scaffolder template.
This will allow users to define properties in the JSON Schema which are templated from current values that have been collected from the user already.
This can be useful when you want to use a value that has already been collected as a default value in another field.
For example:
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: my-template
spec:
parameters:
- title: Some input
description: Get some info from the user
properties:
name:
type: string
default: Test
description:
type: string
default: ${{ parameters.name or "unknown" }}-description
Inclusive of the initial RFC there's been a swarm of issues that are requesting this feature, and we want to align on the implementation and design of this feature.
See the following:
There's some ideas for introducing a templating syntax for both templating into the parameters schema, and also being able to pass through some templating strings to underlying field extensions that can use those templating strings.
We want to align here so that we're not going to have those conflict or compete, and create a standard for how to achieve templating in both circumstances.
parameters section in the scaffolder templates.The proposal is to be able to decorate the template schema server side with a context and use that to drive the form rendering client side.
We can extend the /parameter-schema endpoint to accept a formData context query parameter which will be a JSON object of the current formData state. This in turn allows the scaffolder frontend to repeatedly call the endpoint to get the updated rendered parameter schema. We'll need to turn the endpoint into a POST endpoint to accept the form data, but will retain the GET version for backwards compatibility.
/parameter-schema endpointexport interface ScaffolderApi {
getTemplateParameterSchema(
templateRef: string,
+ formData?: JsonObject,
): Promise<TemplateParameterSchema>;
}
router
- .get(
+ .post(
'/v2/templates/:namespace/:kind/:name/parameter-schema',
async (req, res) => {
const credentials = await httpAuth.credentials(req);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const template = await authorizeTemplate(
req.params,
token,
credentials,
);
const parameters = [template.spec.parameters ?? []].flat();
+ const secureTemplater = await SecureTemplater.loadRenderer({
+ templateFilters: {
+ ...createDefaultFilters({ integrations }),
+ ...additionalTemplateFilters,
+ },
+ templateGlobals: additionalTemplateGlobals,
+ });
+
+ const templatedParameters = parameters.map(parameter =>
+ renderTemplateString(
+ parameter,
+ {
+ parameters: req.body.formData,
+ },
+ secureTemplater,
+ logger,
+ ),
+ );
You can see a quick implementation of this in this branch
default fieldThere's a slight issue with the implementation of the react-jsonschema-form, which makes things like live updating on things like the default field slightly more difficult.
Currently, on first render, the default value is populated and then stored in the formData object or the current state, and the default value is never re-evaluated again at a later stage.
This means that if end users are wanting to set default values with ${{ parameters.myOtherProperty }}, then they would need to ensure that they are on different steps in the form
as the form would need to be re-rendered, and for performance reasons, we don't want to re-render the form on every formData update.
We could fix this, by implementing custom logic for when the parameter-schema is updated, if the updated field is in a default: * field, then we replace the previous value with the new value in the formData automatically.
This is a pretty ugly workaround, but maybe the only option we have. Also at this point, pretty unsure if this affects any other parts of the JSONSchema, and we would also have to implement it for those fields if they exist.
Templating for errorMessages has been solved by using the ajv-errors library https://github.com/backstage/backstage/pull/25624, you can see more about backrefs and pointers here. Any other template strings that will be passed through the underlying components and to be left untemplated should be encapsulated with options instead of passing through raw strings. The below example illustrates an entityAndName format, which under the hood, might do something like ${{ parameters.entity }} - ${{ parameters.name }}, but this implementation never leaks out to the templating language.
parameters:
properties:
...
description:
type: string
default: Test-description
ui:field: CustomDisplayField
ui:options:
format: entityAndName
This change is backwards compatible, and can be released in a minor release. There's no breaking changes to worry about here.
This could lead to confusion as filters such as parseRepoUrl and pick and any custom filters which you define in the backend would not be available in the client side.
Also with the limitations of the default value being updated only on first render and never re-evaluated, there's no performance benefit of doing things client side anymore.
default fieldRather than using a workaround to support re-evaluating the default field, we could instead accept it as a limitation, and document it as such.
This is not desirable, as it is likely a very common use-case to want to template the default field, leading to a poor template creation experience.