From a4cd4051f2836590c4db2f38640058b4ecdd86cc Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Wed, 24 Sep 2025 18:00:01 -0300 Subject: [PATCH 1/3] feat(scaffolder-backend): add defaultEnvironment config to scaffolder Signed-off-by: Rogerio Angeliski --- .changeset/fruity-rivers-arrive.md | 6 + app-config.yaml | 5 + plugins/scaffolder-backend/README.md | 61 ++++++++++ plugins/scaffolder-backend/config.d.ts | 21 ++++ .../src/lib/defaultEnvironment.test.ts | 87 +++++++++++++ .../src/lib/defaultEnvironment.ts | 66 ++++++++++ .../src/scaffolder/dryrun/createDryRunner.ts | 3 + .../tasks/NunjucksWorkflowRunner.test.ts | 115 +++++++++++++++++- .../tasks/NunjucksWorkflowRunner.ts | 69 ++++++++++- .../tasks/StorageTaskBroker.test.ts | 18 ++- .../src/scaffolder/tasks/types.ts | 4 + .../scaffolder-backend/src/service/router.ts | 1 + 12 files changed, 450 insertions(+), 6 deletions(-) create mode 100644 .changeset/fruity-rivers-arrive.md create mode 100644 plugins/scaffolder-backend/src/lib/defaultEnvironment.test.ts create mode 100644 plugins/scaffolder-backend/src/lib/defaultEnvironment.ts diff --git a/.changeset/fruity-rivers-arrive.md b/.changeset/fruity-rivers-arrive.md new file mode 100644 index 0000000000..3e391ae789 --- /dev/null +++ b/.changeset/fruity-rivers-arrive.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-node': patch +--- + +Add `defaultEnvironment` config to scaffolder to enable more flexible and custom templates. Now it's possible enable access to default parameters and secrets in templates, improving security and reducing complexity. diff --git a/app-config.yaml b/app-config.yaml index eacf9a96aa..bd3ebd87ff 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -207,6 +207,11 @@ scaffolder: email: scaffolder@backstage.io # Use to customize the default commit message when new components are created defaultCommitMessage: 'Initial commit' + defaultEnvironment: + parameters: + region: eu-west-1 + secrets: + environment: ${NODE_ENV} auth: experimentalDynamicClientRegistration: diff --git a/plugins/scaffolder-backend/README.md b/plugins/scaffolder-backend/README.md index 34a378ba44..3c4a2cb283 100644 --- a/plugins/scaffolder-backend/README.md +++ b/plugins/scaffolder-backend/README.md @@ -64,6 +64,67 @@ you will not have any templates available to use. These need to be [added to the To get up and running and try out some templates quickly, you can or copy the catalog locations from the [create-app template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs). +## Configuration + +### Default Environment + +The scaffolder supports a `defaultEnvironment` configuration that provides default parameters and secrets to all templates. This reduces template complexity and improves security by centralizing common values. + +```yaml +scaffolder: + defaultEnvironment: + parameters: + region: eu-west-1 + organizationName: acme-corp + defaultRegistry: registry.acme-corp.com + secrets: + AWS_ACCESS_KEY: ${AWS_ACCESS_KEY} + GITHUB_TOKEN: ${GITHUB_TOKEN} + DOCKER_REGISTRY_TOKEN: ${DOCKER_REGISTRY_TOKEN} +``` + +#### Default parameters + +Default parameters are accessible via `${{ environment.parameters.* }}` in templates. Default parameters are isolated in their own context to avoid naming conflicts. + +```yaml + parameters: + - title: Fill in some steps + required: + - organizationName + properties: + organizationName: + title: organizationName + type: string + description: Unique name of the organization + ui:autofocus: true + ui:options: + rows: 5 + + steps: + - id: deploy + name: Deploy Application + action: aws:deploy + input: + region: ${{ environment.parameters.region }} # Resolves to defaultEnvironment.parameters.region + organization: ${{ parameters.organizationName }} # Resolves to frontend input value + otherOrganization: ${{ environment.parameters.organizationName }} # Resolves to defaultEnvironment.parameters.organizationName +``` + +#### Secrets + +Default secrets are resolved from environment variables and accessible via `${{ environment.secrets.* }}` in template actions. Secrets are only available during action execution, not in frontend forms. + +```yaml +- id: deploy + name: Deploy with credentials + action: aws:deploy + input: + accessKey: ${{ environment.secrets.AWS_ACCESS_KEY }} # Resolves to defaultEnvironment.secrets.AWS_ACCESS_KEY +``` + +**Security Note:** Secrets are automatically masked in logs and are only available to backend actions, never exposed to the frontend. + ## Audit Events The Scaffolder backend emits audit events for various operations. Events are grouped logically by `eventId`, with `subEventId` providing further distinction when needed. diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index b4fb532901..866ededb50 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -108,5 +108,26 @@ export interface Config { auditor?: { taskParameterMaxLength?: number; }; + + /** + * Default environment variables and secrets available to all templates. + */ + defaultEnvironment?: { + /** + * Default parameters accessible via ${{ environment.parameters.* }} in templates. + */ + parameters?: { + [key: string]: string; + }; + + /** + * Secret values from environment variables accessible via ${{ environment.secrets.* }} in templates. + * Values should reference environment variables like ${SECRET_NAME}. + * @visibility secret + */ + secrets?: { + [key: string]: string; + }; + }; }; } diff --git a/plugins/scaffolder-backend/src/lib/defaultEnvironment.test.ts b/plugins/scaffolder-backend/src/lib/defaultEnvironment.test.ts new file mode 100644 index 0000000000..c47863e0a2 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/defaultEnvironment.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { resolveDefaultEnvironment } from './defaultEnvironment'; + +describe('defaultEnvironment', () => { + describe('resolveDefaultEnvironment', () => { + it('should return empty when no defaultEnvironment config is provided', () => { + const config = new ConfigReader({}); + const result = resolveDefaultEnvironment(config); + + expect(result).toEqual({ + parameters: {}, + secrets: {}, + }); + }); + + it('should resolve parameters and secrets from config', () => { + const config = new ConfigReader({ + scaffolder: { + defaultEnvironment: { + parameters: { + region: 'us-east-1', + organizationName: 'acme-corp', + version: '1.0.0', + }, + secrets: { + AWS_ACCESS_KEY: 'test-secret-value', + DATABASE_PASSWORD: 'db-password', + LITERAL_SECRET: 'literal-value', + }, + }, + }, + }); + + const result = resolveDefaultEnvironment(config); + + expect(result).toEqual({ + parameters: { + region: 'us-east-1', + organizationName: 'acme-corp', + version: '1.0.0', + }, + secrets: { + AWS_ACCESS_KEY: 'test-secret-value', + DATABASE_PASSWORD: 'db-password', + LITERAL_SECRET: 'literal-value', + }, + }); + }); + + it('should handle only parameters without secrets', () => { + const config = new ConfigReader({ + scaffolder: { + defaultEnvironment: { + parameters: { + region: 'eu-west-1', + }, + }, + }, + }); + + const result = resolveDefaultEnvironment(config); + + expect(result).toEqual({ + parameters: { + region: 'eu-west-1', + }, + secrets: {}, + }); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/lib/defaultEnvironment.ts b/plugins/scaffolder-backend/src/lib/defaultEnvironment.ts new file mode 100644 index 0000000000..cfb47a0130 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/defaultEnvironment.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; + +export interface DefaultEnvironmentConfig { + parameters?: Record; + secrets?: Record; +} + +export interface ResolvedDefaultEnvironment { + parameters: JsonObject; + secrets: Record; +} + +export function resolveDefaultEnvironment( + config: Config, +): ResolvedDefaultEnvironment { + const defaultEnvConfig = config.getOptionalConfig( + 'scaffolder.defaultEnvironment', + ); + if (!defaultEnvConfig) { + return { + parameters: {}, + secrets: {}, + }; + } + + const parameters: JsonObject = {}; + const secrets: Record = {}; + + const parametersConfig = defaultEnvConfig.getOptionalConfig('parameters'); + if (parametersConfig) { + for (const paramKey of parametersConfig.keys()) { + const paramValue = parametersConfig.getString(paramKey); + parameters[paramKey] = paramValue; + } + } + + const secretsConfig = defaultEnvConfig.getOptionalConfig('secrets'); + if (secretsConfig) { + for (const secretKey of secretsConfig.keys()) { + const secretValue = secretsConfig.getString(secretKey); + secrets[secretKey] = secretValue; + } + } + + return { + parameters, + secrets, + }; +} diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts index d384443252..911afbd231 100644 --- a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts @@ -20,6 +20,7 @@ import { LoggerService, } from '@backstage/backend-plugin-api'; import type { UserEntity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { @@ -79,6 +80,7 @@ export type TemplateTesterCreateOptions = { additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; permissions?: PermissionEvaluator; + config?: Config; }; /** @@ -105,6 +107,7 @@ export function createDryRunner(options: TemplateTesterCreateOptions) { }, }), ]), + config: options.config, }); // Extracting contentsPath and dryRunId from the baseUrl diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 8019bd4134..b43a0142c7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -53,6 +53,16 @@ describe('NunjucksWorkflowRunner', () => { const integrations = ScmIntegrations.fromConfig( new ConfigReader({ + scaffolder: { + defaultEnvironment: { + parameters: { + region: 'us-east-1', + }, + secrets: { + AWS_ACCESS_KEY: 'test-secret-value', + }, + }, + }, integrations: { github: [{ host: 'github.com', token: 'token' }], }, @@ -212,12 +222,26 @@ describe('NunjucksWorkflowRunner', () => { { result: AuthorizeResult.ALLOW }, ]); + const config = new ConfigReader({ + scaffolder: { + defaultEnvironment: { + parameters: { + region: 'us-east-1', + }, + secrets: { + AWS_ACCESS_KEY: 'test-secret-value', + }, + }, + }, + }); + runner = new NunjucksWorkflowRunner({ actionRegistry, integrations, workingDirectory: mockDir.path, logger, permissions: mockedPermissionApi, + config, }); }); @@ -494,6 +518,7 @@ describe('NunjucksWorkflowRunner', () => { action: 'jest-mock-action', input: { foo: '${{parameters.input | lower }}', + region: '${{environment.parameters.region}}', }, }, ], @@ -505,7 +530,9 @@ describe('NunjucksWorkflowRunner', () => { await runner.execute(task); expect(fakeActionHandler).toHaveBeenCalledWith( - expect.objectContaining({ input: { foo: 'backstage' } }), + expect.objectContaining({ + input: { foo: 'backstage', region: 'us-east-1' }, + }), ); }); @@ -777,6 +804,49 @@ describe('NunjucksWorkflowRunner', () => { expectTaskLog('info: ***'); }); + // eslint-disable-next-line jest/expect-expect + it('should redact secrets that are passed in the environment', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: async ctx => { + ctx.logger.info(ctx.input.secret); + }, + schema: { + input: { + type: 'object', + required: ['secret'], + properties: { + secret: { + type: 'string', + }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + secret: '${{ environment.secrets.AWS_ACCESS_KEY }}', + }, + }, + ], + }, + { secret: 'my-secret-value' }, + ); + + await runner.execute(task); + + expectTaskLog('info: ***'); + }); + // eslint-disable-next-line jest/expect-expect it('should redact meta fields properly', async () => { actionRegistry.register({ @@ -1083,7 +1153,9 @@ describe('NunjucksWorkflowRunner', () => { await runner.execute(task); expect(fakeActionHandler).toHaveBeenCalledWith( - expect.objectContaining({ secrets: { foo: 'bar' } }), + expect.objectContaining({ + secrets: { foo: 'bar' }, + }), ); }); @@ -1097,6 +1169,7 @@ describe('NunjucksWorkflowRunner', () => { action: 'jest-mock-action', input: { b: '${{ secrets.foo }}', + aws_key: '${{ environment.secrets.AWS_ACCESS_KEY }}', }, }, ], @@ -1107,7 +1180,41 @@ describe('NunjucksWorkflowRunner', () => { await runner.execute(task); expect(fakeActionHandler).toHaveBeenCalledWith( - expect.objectContaining({ input: { b: 'bar' } }), + expect.objectContaining({ + input: { b: 'bar', aws_key: 'test-secret-value' }, + }), + ); + }); + + it('should separate task secrets from environment secrets', async () => { + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-mock-action', + input: { + b: '${{ secrets.foo }}', + aws_key: '${{ secrets.AWS_ACCESS_KEY }}', + env_aws_key: '${{ environment.secrets.AWS_ACCESS_KEY }}', + }, + }, + ], + }, + { foo: 'bar', AWS_ACCESS_KEY: 'another-value-from-task' }, + ); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + b: 'bar', + aws_key: 'another-value-from-task', + env_aws_key: 'test-secret-value', + }, + }), ); }); @@ -1126,6 +1233,7 @@ describe('NunjucksWorkflowRunner', () => { ], output: { b: '${{ secrets.foo }}', + c: '${{ environment.secrets.AWS_ACCESS_KEY }}', }, }, { foo: 'bar' }, @@ -1134,6 +1242,7 @@ describe('NunjucksWorkflowRunner', () => { const executedTask = await runner.execute(task); expect(executedTask.output.b).toBeUndefined(); + expect(executedTask.output.c).toBeUndefined(); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index bcae93fb4d..cd17c1b169 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -51,6 +51,7 @@ import { createConditionAuthorizer } from '@backstage/plugin-permission-node'; import { actionExecutePermission } from '@backstage/plugin-scaffolder-common/alpha'; import { TaskContext, + TaskSecrets, TemplateAction, TemplateFilter, TemplateGlobal, @@ -64,6 +65,8 @@ import { CheckpointState, CheckpointContext, } from '@backstage/plugin-scaffolder-node/alpha'; +import { Config } from '@backstage/config'; +import { resolveDefaultEnvironment } from '../../lib/defaultEnvironment'; type NunjucksWorkflowRunnerOptions = { workingDirectory: string; @@ -74,10 +77,12 @@ type NunjucksWorkflowRunnerOptions = { additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; permissions?: PermissionsService; + config?: Config; }; type TemplateContext = { parameters: JsonObject; + environment: JsonObject; EXPERIMENTAL_recovery?: TaskRecovery; steps: { [stepName: string]: { output: { [outputName: string]: JsonValue } }; @@ -103,10 +108,12 @@ const createStepLogger = ({ task, step, rootLogger, + secretsForRedaction, }: { task: TaskContext; step: TaskStep; rootLogger: LoggerService; + secretsForRedaction?: string[]; }) => { const taskLogger = WinstonLogger.create({ level: process.env.LOG_LEVEL || 'info', @@ -118,6 +125,7 @@ const createStepLogger = ({ }); taskLogger.addRedactions(Object.values(task.secrets ?? {})); + taskLogger.addRedactions(Object.values(secretsForRedaction ?? {})); return { taskLogger }; }; @@ -128,6 +136,10 @@ const isActionAuthorized = createConditionAuthorizer( export class NunjucksWorkflowRunner implements WorkflowRunner { private readonly defaultTemplateFilters: Record; + private environment: { + parameters: JsonObject; + secrets?: Record; + } = { parameters: {}, secrets: {} }; constructor(private readonly options: NunjucksWorkflowRunnerOptions) { this.defaultTemplateFilters = convertFiltersToRecord( @@ -139,6 +151,24 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { private readonly tracker = scaffoldingTracker(); + async getEnvironmentConfig(): Promise<{ + parameters: JsonObject; + secrets?: TaskSecrets; + }> { + if (this.options.config) { + const defaultEnvironment = resolveDefaultEnvironment(this.options.config); + return { + parameters: defaultEnvironment.parameters, + secrets: defaultEnvironment.secrets, + }; + } + + return { + parameters: {}, + secrets: {}, + }; + } + private isSingleTemplateString(input: string) { const { parser, nodes } = nunjucks as unknown as { parser: { @@ -250,18 +280,32 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { task, step, rootLogger: this.options.logger, + secretsForRedaction: this.environment?.secrets + ? Object.values(this.environment.secrets) + : [], }); if (task.isDryRun) { const redactedSecrets = Object.fromEntries( Object.entries(task.secrets ?? {}).map(secret => [secret[0], '***']), ); + + const redactedEnvironmentSecrets = Object.fromEntries( + Object.entries(this.environment?.secrets ?? {}).map(secret => [ + secret[0], + '***', + ]), + ); const debugInput = (step.input && this.render( step.input, { ...context, + environment: { + parameters: this.environment?.parameters || {}, + secrets: redactedEnvironmentSecrets, + }, secrets: redactedSecrets, }, renderTemplate, @@ -296,7 +340,14 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { step.each && this.render( step.each, - { ...context, secrets: task.secrets ?? {} }, + { + ...context, + environment: { + parameters: this.environment?.parameters || {}, + secrets: this.environment?.secrets ?? {}, + }, + secrets: task?.secrets ?? {}, + }, renderTemplate, ); @@ -318,7 +369,15 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { input: step.input ? this.render( step.input, - { ...context, secrets: task.secrets ?? {}, ...i }, + { + ...context, + environment: { + parameters: this.environment?.parameters ?? {}, + secrets: this.environment?.secrets ?? {}, + }, + secrets: task.secrets ?? {}, + ...i, + }, renderTemplate, ) : {}, @@ -481,6 +540,8 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const { additionalTemplateFilters, additionalTemplateGlobals } = this.options; + this.environment = await this.getEnvironmentConfig(); + const renderTemplate = await SecureTemplater.loadRenderer({ templateFilters: { ...this.defaultTemplateFilters, @@ -497,6 +558,10 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const context: TemplateContext = { parameters: task.spec.parameters, + environment: { + parameters: this.environment?.parameters || {}, + secrets: {}, + }, steps: {}, user: task.spec.user, context: { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 2b13130908..d162f2abbf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -103,6 +103,22 @@ describe('StorageTaskBroker', () => { expect(task.secrets).toEqual(fakeSecrets); }, 10000); + it('should return secrets with priority over defaults', async () => { + const broker = new StorageTaskBroker(storage, logger); + await broker.dispatch(emptyTaskWithFakeSecretsSpec); + const task = await broker.claim(); + + expect(task.secrets).toEqual(fakeSecrets); + }, 10000); + + it('should return all secrets', async () => { + const broker = new StorageTaskBroker(storage, logger); + await broker.dispatch(emptyTaskWithFakeSecretsSpec); + const task = await broker.claim(); + + expect(task.secrets).toEqual({ ...fakeSecrets }); + }, 10000); + it('should complete a task', async () => { const broker = new StorageTaskBroker(storage, logger); const dispatchResult = await broker.dispatch(emptyTaskSpec); @@ -232,7 +248,7 @@ describe('StorageTaskBroker', () => { id: taskId, }), ]), - totalTasks: 13, + totalTasks: 15, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 874e32b838..22882e8f71 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -160,6 +160,10 @@ export type WorkflowResponse = { output: { [key: string]: JsonValue } }; export interface WorkflowRunner { execute(task: TaskContext): Promise; + getEnvironmentConfig?(): Promise<{ + parameters: JsonObject; + secrets?: TaskSecrets; + }>; } export type TaskTrackType = { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 2a0792346d..34f27cb1eb 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -368,6 +368,7 @@ export async function createRouter( auditor, workingDirectory, permissions, + config, ...templateExtensions, }); From bf15d2a2acf4dca35abbb78c5b26fc3294544db9 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 9 Oct 2025 10:38:04 -0300 Subject: [PATCH 2/3] docs: improve software template page Signed-off-by: Rogerio Angeliski --- .../software-templates/configuration.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index a692335ffe..bbf35557ce 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -48,6 +48,65 @@ add the `repoVisibility` key within a software template: repoVisibility: public # or 'internal' or 'private' ``` +### Default Environment + +The scaffolder supports a `defaultEnvironment` configuration that provides default parameters and secrets to all templates. This reduces template complexity and improves security by centralizing common values. + +```yaml +scaffolder: + defaultEnvironment: + parameters: + region: eu-west-1 + organizationName: acme-corp + defaultRegistry: registry.acme-corp.com + secrets: + AWS_ACCESS_KEY: ${AWS_ACCESS_KEY} + GITHUB_TOKEN: ${GITHUB_TOKEN} + DOCKER_REGISTRY_TOKEN: ${DOCKER_REGISTRY_TOKEN} +``` + +#### Default parameters + +Default parameters are accessible via `${{ environment.parameters.* }}` in templates. Default parameters are isolated in their own context to avoid naming conflicts. + +```yaml + parameters: + - title: Fill in some steps + required: + - organizationName + properties: + organizationName: + title: organizationName + type: string + description: Unique name of the organization + ui:autofocus: true + ui:options: + rows: 5 + + steps: + - id: deploy + name: Deploy Application + action: aws:deploy + input: + region: ${{ environment.parameters.region }} # Resolves to defaultEnvironment.parameters.region + organization: ${{ parameters.organizationName }} # Resolves to frontend input value + otherOrganization: ${{ environment.parameters.organizationName }} # Resolves to defaultEnvironment.parameters.organizationName +``` + +#### Secrets + +Default secrets are resolved from environment variables and accessible via `${{ environment.secrets.* }}` in template actions. Secrets are only available during action execution, not in frontend forms. + +```yaml +- id: deploy + name: Deploy with credentials + action: aws:deploy + input: + accessKey: ${{ environment.secrets.AWS_ACCESS_KEY }} # Resolves to defaultEnvironment.secrets.AWS_ACCESS_KEY +``` + +**Security Note:** Secrets are automatically masked in logs and are only available to backend actions, never exposed to the frontend. + ## Disabling Docker in Docker situation (Optional) Software templates use the `fetch:template` action by default, which requires no From 849c0bcda45b36b955bd0b316d0b2bfab3b6e5b0 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 13 Nov 2025 08:52:01 -0300 Subject: [PATCH 3/3] chore: apply revision Signed-off-by: Rogerio Angeliski --- .changeset/fruity-rivers-arrive.md | 1 - .../src/scaffolder/tasks/NunjucksWorkflowRunner.ts | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.changeset/fruity-rivers-arrive.md b/.changeset/fruity-rivers-arrive.md index 3e391ae789..a816946bcc 100644 --- a/.changeset/fruity-rivers-arrive.md +++ b/.changeset/fruity-rivers-arrive.md @@ -1,6 +1,5 @@ --- '@backstage/plugin-scaffolder-backend': minor -'@backstage/plugin-scaffolder-node': patch --- Add `defaultEnvironment` config to scaffolder to enable more flexible and custom templates. Now it's possible enable access to default parameters and secrets in templates, improving security and reducing complexity. diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index cd17c1b169..1a51b3ddf6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -108,12 +108,12 @@ const createStepLogger = ({ task, step, rootLogger, - secretsForRedaction, + redactions, }: { task: TaskContext; step: TaskStep; rootLogger: LoggerService; - secretsForRedaction?: string[]; + redactions?: Record; }) => { const taskLogger = WinstonLogger.create({ level: process.env.LOG_LEVEL || 'info', @@ -124,8 +124,7 @@ const createStepLogger = ({ transports: [new BackstageLoggerTransport(rootLogger, task, step.id)], }); - taskLogger.addRedactions(Object.values(task.secrets ?? {})); - taskLogger.addRedactions(Object.values(secretsForRedaction ?? {})); + taskLogger.addRedactions(Object.values(redactions ?? {})); return { taskLogger }; }; @@ -280,9 +279,10 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { task, step, rootLogger: this.options.logger, - secretsForRedaction: this.environment?.secrets - ? Object.values(this.environment.secrets) - : [], + redactions: { + ...task.secrets, + ...this.environment?.secrets, + }, }); if (task.isDryRun) {