diff --git a/.changeset/swift-brooms-bet.md b/.changeset/swift-brooms-bet.md new file mode 100644 index 0000000000..818284fda9 --- /dev/null +++ b/.changeset/swift-brooms-bet.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Enable usage of secrets within 'each' step of software templates. For example, you can now structure your `each` step like this: + +``` +each: + [ + { name: "Service1", token: "${{ secrets.token1 }}" }, + { name: "Service2", token: "${{ secrets.token2 }}" }, + ] +``` diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index eeaa4ad203..275d5c26b2 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -274,6 +274,58 @@ spec: password: ${{ secrets.password }} ``` +You can also consume secrets within `each` step of the template. + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: v1beta3-demo + title: Test Action template + description: scaffolder v1beta3 template demo +spec: + owner: backstage/techdocs-core + type: service + + parameters: + - title: Authentication + description: Provide authentication for the resource + required: + - service1 + - token1 + - service2 + - token2 + properties: + service1: + type: string + token1: + type: string + ui:field: Secret + service2: + type: string + token2: + type: string + ui:field: Secret + + steps: + - id: setupAuthentication + action: auth:create + each: + [ + { + name: '${{ parameters.service1 }}', + token: '${{ secrets.token1 }}', + }, + { + name: '${{ parameters.service2 }}', + token: '${{ secrets.token2 }}', + }, + ] + input: + name: ${{ each.value.name }} + token: ${{ each.value.token }} +``` + ### Custom step layouts If you find that the default layout of the form used in a particular step does not meet your needs then you can supply your own [custom step layout](./writing-custom-step-layouts.md). diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index a4876f2edb..79a847c46d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -832,6 +832,42 @@ describe('NunjucksWorkflowRunner', () => { }); }); + it('should run a step repeatedly - flat values with secrets', async () => { + const secrets = { + s1: 'secret-value1', + s2: 'secret-value2', + s3: 'secret-value3', + }; + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + each: [ + '${{ secrets.s1 }}', + '${{ secrets.s2 }}', + '${{ secrets.s3 }}', + ], + action: 'jest-mock-action', + input: { secret: '${{each.value}}' }, + }, + ], + }, + secrets, + ); + await runner.execute(task); + + Object.values(secrets).forEach((secret, idx) => { + expectTaskLog( + `info: Running step each: {"key":"${idx}","value":"***"}`, + ); + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ input: { secret } }), + ); + }); + }); + it('should run a step repeatedly - object list', async () => { const task = createMockTaskWithSpec({ steps: [ @@ -862,6 +898,46 @@ describe('NunjucksWorkflowRunner', () => { ); }); + it('should run a step repeatedly - object list with secrets', async () => { + const secrets = { + s1: 'secret-value1', + s2: 'secret-value2', + }; + const names = ['Service1', 'Service2']; + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + each: [ + { name: names[0], token: '${{ secrets.s1 }}' }, + { name: names[1], token: '${{ secrets.s2 }}' }, + ], + action: 'jest-mock-action', + input: { + name: '${{each.value.name}}', + token: '${{each.value.token}}', + }, + }, + ], + }, + secrets, + ); + await runner.execute(task); + + Object.values(secrets).forEach((secret, idx) => { + expectTaskLog( + `info: Running step each: {"key":"${idx}","value":"[object Object]"}`, + ); + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ + input: { name: names[idx], token: secret }, + }), + ); + }); + }); + it('should run a step repeatedly - object', async () => { const settings = { color: 'blue', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index a66ae0e789..7802a7ab8d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -299,7 +299,12 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } const resolvedEach = - step.each && this.render(step.each, context, renderTemplate); + step.each && + this.render( + step.each, + { ...context, secrets: task.secrets ?? {} }, + renderTemplate, + ); if (step.each && !resolvedEach) { throw new InputError(