diff --git a/.changeset/silver-ties-marry.md b/.changeset/silver-ties-marry.md new file mode 100644 index 0000000000..2a3b01f2fc --- /dev/null +++ b/.changeset/silver-ties-marry.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Added the ability to support supplying secrets when creating tasks in the `scaffolder-backend`. + +**deprecation**: Deprecated `ctx.token` from actions in the `scaffolder-backend`. Please move to using `ctx.secrets.backstageToken` instead. + +**deprecation**: Deprecated `task.token` in `TaskSpec` in the `scaffolder-backend`. Please move to using `task.secrets.backstageToken` instead. diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index ee84e06ae9..12b29cae39 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -43,6 +43,7 @@ export type ActionContext = { logger: Logger_2; logStream: Writable; token?: string | undefined; + secrets?: TaskSecrets; workspacePath: string; input: Input; output(name: string, value: JsonValue): void; @@ -439,8 +440,9 @@ export class TaskManager implements TaskContext { } // @public -export type TaskSecrets = { - token: string | undefined; +export type TaskSecrets = Record & { + token?: string; + backstageToken?: string; }; export { TaskSpec }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts index 249939b0e6..65d0e1b1e7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -110,7 +110,9 @@ export function createCatalogRegisterAction(options: { type: 'url', target: catalogInfoUrl, }, - ctx.token ? { token: ctx.token } : {}, + ctx.secrets?.backstageToken + ? { token: ctx.secrets.backstageToken } + : {}, ); try { @@ -120,7 +122,9 @@ export function createCatalogRegisterAction(options: { type: 'url', target: catalogInfoUrl, }, - ctx.token ? { token: ctx.token } : {}, + ctx.secrets?.backstageToken + ? { token: ctx.secrets.backstageToken } + : {}, ); if (result.entities.length > 0) { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 52149be724..4b842e8c5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -18,7 +18,7 @@ import { Logger } from 'winston'; import { Writable } from 'stream'; import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; -import { TemplateMetadata } from '../tasks/types'; +import { TaskSecrets, TemplateMetadata } from '../tasks/types'; type PartialJsonObject = Partial; type PartialJsonValue = PartialJsonObject | JsonValue | undefined; @@ -35,8 +35,10 @@ export type ActionContext = { /** * User token forwarded from initial request, for use in subsequent api requests + * @deprecated use `secrets.backstageToken` instead */ token?: string | undefined; + secrets?: TaskSecrets; workspacePath: string; input: Input; output(name: string, value: JsonValue): void; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 813d8d0f5b..dcda3b921e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -43,7 +43,7 @@ export type RawDbTaskRow = { status: Status; last_heartbeat_at?: string; created_at: string; - secrets?: string; + secrets?: string | null; }; export type RawDbTaskEventRow = { @@ -139,6 +139,8 @@ export class DatabaseTaskStore implements TaskStore { .update({ status: 'processing', last_heartbeat_at: this.db.fn.now(), + // remove the secrets when moving moving to processing state. + secrets: null, }); if (updateCount < 1) { @@ -235,8 +237,8 @@ export class DatabaseTaskStore implements TaskStore { }) .update({ status, - secrets: null as any, }); + if (updateCount !== 1) { throw new ConflictError( `Failed to update status to '${status}' for taskId ${taskId}`, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts index 5582b53023..91c82edebb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts @@ -236,7 +236,9 @@ export class HandlebarsWorkflowRunner implements WorkflowRunner { logger: taskLogger, logStream: stream, input, + // this token is deprecated, and will be removed in favour of secrets.backstageToken instead. token: task.secrets?.token, + secrets: task.secrets ?? {}, workspacePath, async createTemporaryDirectory() { const tmpDir = await fs.mkdtemp( diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index aace2cb437..161d5b865b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -472,6 +472,33 @@ describe('DefaultWorkflowRunner', () => { }); }); + describe('secrets', () => { + it('should pass through the secrets to the context', async () => { + const task = createMockTaskWithSpec( + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-mock-action', + input: {}, + }, + ], + output: {}, + parameters: {}, + }, + { foo: 'bar' }, + ); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ secrets: { foo: 'bar' } }), + ); + }); + }); + describe('filters', () => { it('provides the parseRepoUrl filter', async () => { const task = createMockTaskWithSpec({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 71d53cc7ec..46f87c1d00 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -257,7 +257,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { await action.handler({ baseUrl: task.spec.baseUrl, input, + // this token is deprecated, and will be removed in favour of secrets.backstageToken instead. token: task.secrets?.token, + secrets: task.secrets ?? {}, logger: taskLogger, logStream: streamLogger, workspacePath, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 76fb8e00f0..130f6ced13 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -94,13 +94,12 @@ describe('StorageTaskBroker', () => { expect(taskRow.status).toBe('completed'); }, 10000); - it('should remove secrets after completing a task', async () => { + it('should remove secrets after picking up a task', async () => { const broker = new StorageTaskBroker(storage, logger); const dispatchResult = await broker.dispatch({} as TaskSpec, fakeSecrets); - const task = await broker.claim(); - await task.complete('completed'); + await broker.claim(); + const taskRow = await storage.getTask(dispatchResult.taskId); - expect(taskRow.status).toBe('completed'); expect(taskRow.secrets).toBeUndefined(); }, 10000); @@ -113,16 +112,6 @@ describe('StorageTaskBroker', () => { expect(taskRow.status).toBe('failed'); }); - it('should remove secrets after failing a task', async () => { - const broker = new StorageTaskBroker(storage, logger); - const dispatchResult = await broker.dispatch({} as TaskSpec, fakeSecrets); - const task = await broker.claim(); - await task.complete('failed'); - const taskRow = await storage.getTask(dispatchResult.taskId); - expect(taskRow.status).toBe('failed'); - expect(taskRow.secrets).toBeUndefined(); - }); - it('multiple brokers should be able to observe a single task', async () => { const broker1 = new StorageTaskBroker(storage, logger); const broker2 = new StorageTaskBroker(storage, logger); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 1a69f28fb6..8caee87f57 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -89,8 +89,10 @@ export type SerializedTaskEvent = { * * @public */ -export type TaskSecrets = { - token: string | undefined; +export type TaskSecrets = Record & { + /** @deprecated Use `backstageToken` instead */ + token?: string; + backstageToken?: string; }; /** diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9d26857ab9..f4f7398c8a 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -232,6 +232,9 @@ export async function createRouter( } const result = await taskBroker.dispatch(taskSpec, { + ...req.body.secrets, + backstageToken: token, + // This is deprecated, but we need to support it for now if people are running their own task broker. token: token, }); diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 8443de563e..d2bbb2a5e1 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -192,7 +192,11 @@ export interface ScaffolderApi { // // (undocumented) listActions(): Promise; - scaffold(templateName: string, values: Record): Promise; + scaffold( + templateName: string, + values: Record, + secrets?: Record, + ): Promise; // Warning: (ae-forgotten-export) The symbol "LogEvent" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -236,7 +240,11 @@ export class ScaffolderClient implements ScaffolderApi { ): Promise; // (undocumented) listActions(): Promise; - scaffold(templateName: string, values: Record): Promise; + scaffold( + templateName: string, + values: Record, + secrets?: Record, + ): Promise; // (undocumented) streamLogs(opts: { taskId: string; after?: number }): Observable; } diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 70ed803ef7..7f096a02f7 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -69,8 +69,13 @@ export interface ScaffolderApi { * * @param templateName - Name of the Template entity for the scaffolder to use. New project is going to be created out of this template. * @param values - Parameters for the template, e.g. name, description + * @param secrets - Optional secrets to pass to as the secrets parameter to the template. */ - scaffold(templateName: string, values: Record): Promise; + scaffold( + templateName: string, + values: Record, + secrets?: Record, + ): Promise; getTask(taskId: string): Promise; @@ -151,10 +156,12 @@ export class ScaffolderClient implements ScaffolderApi { * * @param templateName - Template name for the scaffolder to use. New project is going to be created out of this template. * @param values - Parameters for the template, e.g. name, description + * @param secrets - Optional secrets to pass to as the secrets parameter to the template. */ async scaffold( templateName: string, values: Record, + secrets: Record = {}, ): Promise { const { token } = await this.identityApi.getCredentials(); const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`; @@ -164,7 +171,11 @@ export class ScaffolderClient implements ScaffolderApi { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }), }, - body: JSON.stringify({ templateName, values: { ...values } }), + body: JSON.stringify({ + templateName, + values: { ...values }, + secrets, + }), }); if (response.status !== 201) {