From d48f69fa60aba165eeaf7249489ec0f44e60890c Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 2 May 2022 11:16:13 +0200 Subject: [PATCH 1/9] chore: working added createdBy column Signed-off-by: blam --- .../migrations/20220129100000_created_by.js | 38 +++++++ .../src/scaffolder/tasks/DatabaseTaskStore.ts | 4 + .../src/scaffolder/tasks/StorageTaskBroker.ts | 5 + .../src/scaffolder/tasks/types.ts | 3 + .../scaffolder-backend/src/service/router.ts | 29 ++++- .../Stepper/Stepper copy.tsx | 102 ++++++++++++++++++ 6 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 plugins/scaffolder-backend/migrations/20220129100000_created_by.js create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper copy.tsx diff --git a/plugins/scaffolder-backend/migrations/20220129100000_created_by.js b/plugins/scaffolder-backend/migrations/20220129100000_created_by.js new file mode 100644 index 0000000000..3391a883c2 --- /dev/null +++ b/plugins/scaffolder-backend/migrations/20220129100000_created_by.js @@ -0,0 +1,38 @@ +/* + * Copyright 2020 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('tasks', table => { + table + .text('created_by') + .nullable() + .comment('An entityRef of the user who created the task'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('tasks', table => { + table.dropColumn('created_by'); + }); +}; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 9fef68529f..d0a0fec0a6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -43,6 +43,7 @@ export type RawDbTaskRow = { status: TaskStatus; last_heartbeat_at?: string; created_at: string; + created_by: string | null; secrets?: string | null; }; @@ -100,6 +101,7 @@ export class DatabaseTaskStore implements TaskStore { status: result.status, lastHeartbeatAt: result.last_heartbeat_at, createdAt: result.created_at, + createdBy: result.created_by ?? undefined, secrets, }; } catch (error) { @@ -115,6 +117,7 @@ export class DatabaseTaskStore implements TaskStore { id: taskId, spec: JSON.stringify(options.spec), secrets: options.secrets ? JSON.stringify(options.secrets) : undefined, + created_by: options.createdBy ?? null, status: 'open', }); return { taskId }; @@ -155,6 +158,7 @@ export class DatabaseTaskStore implements TaskStore { status: 'processing', lastHeartbeatAt: task.last_heartbeat_at, createdAt: task.created_at, + createdBy: task.created_by ?? undefined, secrets, }; } catch (error) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index a92d6abdb8..0aa0b6b830 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -127,6 +127,10 @@ export interface CurrentClaimedTask { * The secrets that are stored with the task. */ secrets?: TaskSecrets; + /** + * The creator of the task. + */ + createdBy?: string; } function defer() { @@ -156,6 +160,7 @@ export class StorageTaskBroker implements TaskBroker { taskId: pendingTask.id, spec: pendingTask.spec, secrets: pendingTask.secrets, + createdBy: pendingTask.createdBy, }, this.storage, this.logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 2af98b111b..bbd0da2565 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -47,6 +47,7 @@ export type SerializedTask = { status: TaskStatus; createdAt: string; lastHeartbeatAt?: string; + createdBy?: string; secrets?: TaskSecrets; }; @@ -97,6 +98,7 @@ export type TaskBrokerDispatchResult = { export type TaskBrokerDispatchOptions = { spec: TaskSpec; secrets?: TaskSecrets; + createdBy?: string; }; /** @@ -157,6 +159,7 @@ export type TaskStoreListEventsOptions = { */ export type TaskStoreCreateTaskOptions = { spec: TaskSpec; + createdBy?: string; secrets?: TaskSecrets; }; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index e7413b3a87..94c2694f9a 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -130,10 +130,11 @@ export async function createRouter( '/v2/templates/:namespace/:kind/:name/parameter-schema', async (req, res) => { const { namespace, kind, name } = req.params; + const { token } = parseBearerToken(req.headers.authorization); const template = await findTemplate({ catalogApi: catalogClient, entityRef: { kind, namespace, name }, - token: getBearerToken(req.headers.authorization), + token, }); if (isSupportedTemplate(template)) { const parameters = [template.spec.parameters ?? []].flat(); @@ -168,12 +169,16 @@ export async function createRouter( const { kind, namespace, name } = parseEntityRef(templateRef, { defaultKind: 'template', }); + const { token, entityRef: userEntityRef } = parseBearerToken( + req.headers.authorization, + ); + const values = req.body.values; - const token = getBearerToken(req.headers.authorization); + const template = await findTemplate({ catalogApi: catalogClient, entityRef: { kind, namespace, name }, - token: getBearerToken(req.headers.authorization), + token, }); if (!isSupportedTemplate(template)) { @@ -215,6 +220,7 @@ export async function createRouter( const result = await taskBroker.dispatch({ spec: taskSpec, + createdBy: userEntityRef, secrets: { ...req.body.secrets, backstageToken: token, @@ -315,6 +321,19 @@ export async function createRouter( return app; } -function getBearerToken(header?: string): string | undefined { - return header?.match(/Bearer\s+(\S+)/i)?.[1]; +function parseBearerToken(header?: string): { + token?: string; + entityRef?: string; +} { + const token = header?.match(/Bearer\s+(\S+)/i)?.[1]; + + if (!token) return {}; + + const [_header, rawPayload, _signature] = token.split('.'); + const payload: { sub: string } = JSON.parse(atob(rawPayload)); + + return { + entityRef: payload.sub, + token, + }; } diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper copy.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper copy.tsx new file mode 100644 index 0000000000..f864e9ee09 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper copy.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2022 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 { + Stepper as MuiStepper, + Step as MuiStep, + StepLabel as MuiStepLabel, + Button, + makeStyles, +} from '@material-ui/core'; +import { withTheme } from '@rjsf/core'; +import { Theme as MuiTheme } from '@rjsf/material-ui'; +import React, { useMemo, useState } from 'react'; +import { FieldExtensionOptions } from '../../../extensions'; +import { TemplateParameterSchema } from '../../../types'; +import { useTemplateSchema } from './useTemplateSchema'; + +const useStyles = makeStyles(theme => ({ + backButton: { + marginRight: theme.spacing(1), + }, + footer: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'right', + }, + formWrapper: { + padding: theme.spacing(2), + }, +})); + +export interface StepperProps { + manifest: TemplateParameterSchema; + extensions: FieldExtensionOptions[]; +} + +const Form = withTheme(MuiTheme); + +export const Stepper = (props: StepperProps) => { + const { steps } = useTemplateSchema(props.manifest); + const [activeStep, setActiveStep] = useState(0); + const styles = useStyles(); + + const extensions = useMemo(() => { + return Object.fromEntries( + props.extensions.map(({ name, component }) => [name, component]), + ); + }, [props.extensions]); + + const handleBack = () => { + setActiveStep(prevActiveStep => prevActiveStep - 1); + }; + const handleNext = () => { + setActiveStep(prevActiveStep => prevActiveStep + 1); + }; + + return ( + <> + + {steps.map((step, index) => ( + + {step.title} + + ))} + +
+
+
+ + +
+
+
+ + ); +}; From d06a045a682e5a0878ce6df561eef1603e6f0464 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 3 May 2022 17:27:01 +0200 Subject: [PATCH 2/9] chore: added the user entity for decoration and added some more tests for the router which were untested before Signed-off-by: blam --- .../tasks/NunjucksWorkflowRunner.ts | 6 + .../src/scaffolder/tasks/StorageTaskBroker.ts | 4 + .../src/scaffolder/tasks/types.ts | 1 + .../src/service/router.test.ts | 131 ++++++++++++++++-- .../scaffolder-backend/src/service/router.ts | 14 +- 5 files changed, 144 insertions(+), 12 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 0bd5a3c840..c9d0714392 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -37,6 +37,7 @@ import { TaskSpecV1beta3, TaskStep, } from '@backstage/plugin-scaffolder-common'; +import { UserEntity } from '@backstage/catalog-model'; type NunjucksWorkflowRunnerOptions = { workingDirectory: string; @@ -52,6 +53,10 @@ type TemplateContext = { [stepName: string]: { output: { [outputName: string]: JsonValue } }; }; secrets?: Record; + user?: { + entity?: UserEntity; + ref?: string; + }; }; const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta3 => { @@ -203,6 +208,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const context: TemplateContext = { parameters: task.spec.parameters, steps: {}, + user: task.spec.user, }; for (const step of task.spec.steps) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 0aa0b6b830..17f07f702d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -59,6 +59,10 @@ export class TaskManager implements TaskContext { return this.task.secrets; } + get createdBy() { + return this.task.createdBy; + } + async getWorkspaceName() { return this.task.taskId; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index bbd0da2565..a3a9ef84d1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -109,6 +109,7 @@ export type TaskBrokerDispatchOptions = { export interface TaskContext { spec: TaskSpec; secrets?: TaskSecrets; + createdBy?: string; done: boolean; emitLog(message: string, logMetadata?: JsonObject): Promise; complete(result: TaskCompletionState, metadata?: JsonObject): Promise; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 6caed7b0cb..3f3cf1f4d7 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -48,12 +48,11 @@ import request from 'supertest'; */ import { createRouter, DatabaseTaskStore, TaskBroker } from '../index'; import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; -import { stringifyEntityRef } from '@backstage/catalog-model'; - -const createCatalogClient = (template: any) => - ({ - getEntityByRef: async () => template, - } as unknown as CatalogApi); +import { + parseEntityRef, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; function createDatabase(): PluginDatabaseManager { return DatabaseManager.fromConfig( @@ -76,7 +75,9 @@ const mockUrlReader = UrlReaders.default({ describe('createRouter', () => { let app: express.Express; let taskBroker: TaskBroker; - const template: TemplateEntityV1beta3 = { + const catalogClient = { getEntityByRef: jest.fn() } as unknown as CatalogApi; + + const mockTemplate: TemplateEntityV1beta3 = { apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { @@ -105,6 +106,22 @@ describe('createRouter', () => { }, }; + const mockUser: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'guest', + annotations: { + 'google.com/email': 'bobby@tables.com', + }, + }, + spec: { + profile: { + displayName: 'Robert Tables of the North', + }, + }, + }; + beforeEach(async () => { const logger = getVoidLogger(); const databaseTaskStore = await DatabaseTaskStore.create({ @@ -120,11 +137,26 @@ describe('createRouter', () => { logger: getVoidLogger(), config: new ConfigReader({}), database: createDatabase(), - catalogClient: createCatalogClient(template), + catalogClient, reader: mockUrlReader, taskBroker, }); app = express().use(router); + + jest + .spyOn(catalogClient, 'getEntityByRef') + .mockImplementation(async ref => { + const { kind } = parseEntityRef(ref); + + if (kind === 'template') { + return mockTemplate; + } + + if (kind === 'user') { + return mockUser; + } + throw new Error(`no mock found for kind: ${kind}`); + }); }); afterEach(() => { @@ -158,9 +190,8 @@ describe('createRouter', () => { }); it('return the template id', async () => { - ( - taskBroker.dispatch as jest.Mocked['dispatch'] - ).mockResolvedValue({ + const broker = taskBroker.dispatch as jest.Mocked['dispatch']; + broker.mockResolvedValue({ taskId: 'a-random-id', }); @@ -179,6 +210,84 @@ describe('createRouter', () => { expect(response.body.id).toBe('a-random-id'); expect(response.status).toEqual(201); }); + + it('should call the broker with a correct spec', async () => { + const broker = taskBroker.dispatch as jest.Mocked['dispatch']; + const mockToken = + 'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob'; + + await request(app) + .post('/v2/tasks') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); + + expect(broker).toHaveBeenCalledWith( + expect.objectContaining({ + createdBy: 'user:default/guest', + secrets: { + backstageToken: mockToken, + }, + + spec: { + apiVersion: mockTemplate.apiVersion, + steps: mockTemplate.spec.steps.map((step, index) => ({ + ...step, + id: step.id ?? `step-${index + 1}`, + name: step.name ?? step.action, + })), + output: mockTemplate.spec.output ?? {}, + parameters: { + required: 'required-value', + }, + user: { + entity: mockUser, + ref: 'user:default/guest', + }, + templateInfo: { + entityRef: stringifyEntityRef({ + kind: 'Template', + namespace: 'Default', + name: mockTemplate.metadata?.name, + }), + baseUrl: 'https://dev.azure.com', + }, + }, + }), + ); + }); + + it('should not decorate a user when no backstage auth is passed', async () => { + const broker = taskBroker.dispatch as jest.Mocked['dispatch']; + + await request(app) + .post('/v2/tasks') + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); + + expect(broker).toHaveBeenCalledWith( + expect.objectContaining({ + createdBy: undefined, + spec: expect.objectContaining({ + user: { entity: undefined, ref: undefined }, + }), + }), + ); + }); }); describe('GET /v2/tasks/:taskId', () => { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 94c2694f9a..5a6690edab 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -16,7 +16,11 @@ import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; -import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; +import { + parseEntityRef, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { InputError, NotFoundError } from '@backstage/errors'; @@ -173,6 +177,10 @@ export async function createRouter( req.headers.authorization, ); + const userEntity = userEntityRef + ? await catalogClient.getEntityByRef(userEntityRef, { token }) + : undefined; + const values = req.body.values; const template = await findTemplate({ @@ -208,6 +216,10 @@ export async function createRouter( })), output: template.spec.output ?? {}, parameters: values, + user: { + entity: userEntity as UserEntity, + ref: userEntityRef, + }, templateInfo: { entityRef: stringifyEntityRef({ kind, From 277634feb0999fee42245e7e1ea4ddfd53ebf056 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 4 May 2022 14:42:33 +0200 Subject: [PATCH 3/9] chore: added some test for checking that the workflow passes through the user Signed-off-by: blam --- .../tasks/NunjucksWorkflowRunner.test.ts | 35 +++++- plugins/scaffolder-common/src/TaskSpec.ts | 14 +++ .../Stepper/Stepper copy.tsx | 102 ------------------ 3 files changed, 47 insertions(+), 104 deletions(-) delete mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper copy.tsx diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 0ef0846ddb..1cf3f90774 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -24,6 +24,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { TaskContext, TaskSecrets } from './types'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { UserEntity } from '@backstage/catalog-model'; // The Stream module is lazy loaded, so make sure it's in the module cache before mocking fs void winston.transports.Stream; @@ -558,8 +559,8 @@ describe('DefaultWorkflowRunner', () => { }); }); - describe('filters', () => { - it('provides the parseRepoUrl filter', async () => { + describe('user', () => { + it('allows access to the user entity at the templating level', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', steps: [ @@ -587,4 +588,34 @@ describe('DefaultWorkflowRunner', () => { }); }); }); + + describe('filters', () => { + it('provides the parseRepoUrl filter', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'output-action', + input: {}, + }, + ], + user: { + entity: { metadata: { name: 'bob' } } as UserEntity, + ref: 'user:default/guest', + }, + output: { + foo: '${{ user.entity.metadata.name }} ${{ user.ref }}', + }, + parameters: { + repoUrl: 'github.com?repo=repo&owner=owner', + }, + }); + + const { output } = await runner.execute(task); + + expect(output.foo).toEqual('bob user:default/guest'); + }); + }); }); diff --git a/plugins/scaffolder-common/src/TaskSpec.ts b/plugins/scaffolder-common/src/TaskSpec.ts index e745964ff7..4ba43092ae 100644 --- a/plugins/scaffolder-common/src/TaskSpec.ts +++ b/plugins/scaffolder-common/src/TaskSpec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { UserEntity } from '@backstage/catalog-model'; import { JsonValue, JsonObject } from '@backstage/types'; /** @@ -91,6 +92,19 @@ export interface TaskSpecV1beta3 { * Some information about the template that is stored on the task spec. */ templateInfo?: TemplateInfo; + /** + * Some decoration of the author of the task that should be available in the context + */ + user?: { + /** + * The decorated entity from the Catalog + */ + entity?: UserEntity; + /** + * An entity ref for the author of the task + */ + ref?: string; + }; } /** diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper copy.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper copy.tsx deleted file mode 100644 index f864e9ee09..0000000000 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper copy.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2022 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 { - Stepper as MuiStepper, - Step as MuiStep, - StepLabel as MuiStepLabel, - Button, - makeStyles, -} from '@material-ui/core'; -import { withTheme } from '@rjsf/core'; -import { Theme as MuiTheme } from '@rjsf/material-ui'; -import React, { useMemo, useState } from 'react'; -import { FieldExtensionOptions } from '../../../extensions'; -import { TemplateParameterSchema } from '../../../types'; -import { useTemplateSchema } from './useTemplateSchema'; - -const useStyles = makeStyles(theme => ({ - backButton: { - marginRight: theme.spacing(1), - }, - footer: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'right', - }, - formWrapper: { - padding: theme.spacing(2), - }, -})); - -export interface StepperProps { - manifest: TemplateParameterSchema; - extensions: FieldExtensionOptions[]; -} - -const Form = withTheme(MuiTheme); - -export const Stepper = (props: StepperProps) => { - const { steps } = useTemplateSchema(props.manifest); - const [activeStep, setActiveStep] = useState(0); - const styles = useStyles(); - - const extensions = useMemo(() => { - return Object.fromEntries( - props.extensions.map(({ name, component }) => [name, component]), - ); - }, [props.extensions]); - - const handleBack = () => { - setActiveStep(prevActiveStep => prevActiveStep - 1); - }; - const handleNext = () => { - setActiveStep(prevActiveStep => prevActiveStep + 1); - }; - - return ( - <> - - {steps.map((step, index) => ( - - {step.title} - - ))} - -
-
-
- - -
-
-
- - ); -}; From 76a294331c091406fdd0112017a63a4f587c8c12 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 4 May 2022 16:20:57 +0200 Subject: [PATCH 4/9] chore: juggle the code around proper Signed-off-by: blam --- .../tasks/NunjucksWorkflowRunner.test.ts | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 1cf3f90774..6b1701380b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -561,36 +561,6 @@ describe('DefaultWorkflowRunner', () => { describe('user', () => { it('allows access to the user entity at the templating level', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'scaffolder.backstage.io/v1beta3', - steps: [ - { - id: 'test', - name: 'name', - action: 'output-action', - input: {}, - }, - ], - output: { - foo: '${{ parameters.repoUrl | parseRepoUrl }}', - }, - parameters: { - repoUrl: 'github.com?repo=repo&owner=owner', - }, - }); - - const { output } = await runner.execute(task); - - expect(output.foo).toEqual({ - host: 'github.com', - owner: 'owner', - repo: 'repo', - }); - }); - }); - - describe('filters', () => { - it('provides the parseRepoUrl filter', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', steps: [ @@ -618,4 +588,34 @@ describe('DefaultWorkflowRunner', () => { expect(output.foo).toEqual('bob user:default/guest'); }); }); + + describe('filters', () => { + it('provides the parseRepoUrl filter', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'output-action', + input: {}, + }, + ], + output: { + foo: '${{ parameters.repoUrl | parseRepoUrl }}', + }, + parameters: { + repoUrl: 'github.com?repo=repo&owner=owner', + }, + }); + + const { output } = await runner.execute(task); + + expect(output.foo).toEqual({ + host: 'github.com', + owner: 'owner', + repo: 'repo', + }); + }); + }); }); From 5b2d1dd6b427aab7dfb9a5c9ffce3634fdb6f4fc Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 4 May 2022 16:23:43 +0200 Subject: [PATCH 5/9] chore: updating API reports Signed-off-by: blam --- plugins/scaffolder-backend/api-report.md | 8 ++++++++ plugins/scaffolder-common/api-report.md | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 1bc8a27f54..7cf26aa473 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -342,6 +342,7 @@ export type CreateWorkerOptions = { // @public export interface CurrentClaimedTask { + createdBy?: string; secrets?: TaskSecrets; spec: TaskSpec; taskId: string; @@ -476,6 +477,7 @@ export type SerializedTask = { status: TaskStatus; createdAt: string; lastHeartbeatAt?: string; + createdBy?: string; secrets?: TaskSecrets; }; @@ -510,6 +512,7 @@ export interface TaskBroker { export type TaskBrokerDispatchOptions = { spec: TaskSpec; secrets?: TaskSecrets; + createdBy?: string; }; // @public @@ -525,6 +528,8 @@ export interface TaskContext { // (undocumented) complete(result: TaskCompletionState, metadata?: JsonObject): Promise; // (undocumented) + createdBy?: string; + // (undocumented) done: boolean; // (undocumented) emitLog(message: string, logMetadata?: JsonObject): Promise; @@ -550,6 +555,8 @@ export class TaskManager implements TaskContext { logger: Logger, ): TaskManager; // (undocumented) + get createdBy(): string | undefined; + // (undocumented) get done(): boolean; // (undocumented) emitLog(message: string, logMetadata?: JsonObject): Promise; @@ -609,6 +616,7 @@ export interface TaskStore { // @public export type TaskStoreCreateTaskOptions = { spec: TaskSpec; + createdBy?: string; secrets?: TaskSecrets; }; diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 532e72d3fd..a1c5c2bf23 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -7,6 +7,7 @@ import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { KindValidator } from '@backstage/catalog-model'; +import { UserEntity } from '@backstage/catalog-model'; // @public export type TaskSpec = TaskSpecV1beta3; @@ -20,6 +21,10 @@ export interface TaskSpecV1beta3 { parameters: JsonObject; steps: TaskStep[]; templateInfo?: TemplateInfo; + user?: { + entity?: UserEntity; + ref?: string; + }; } // @public From f8baf7df44150c630df05a20264b8950e99c8aca Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 4 May 2022 16:24:55 +0200 Subject: [PATCH 6/9] chore: added changeset Signed-off-by: blam --- .changeset/gentle-penguins-kick.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/gentle-penguins-kick.md diff --git a/.changeset/gentle-penguins-kick.md b/.changeset/gentle-penguins-kick.md new file mode 100644 index 0000000000..c19834c6af --- /dev/null +++ b/.changeset/gentle-penguins-kick.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-common': minor +--- + +Added the ability to reference the user in the `template.yaml` manifest From cd03a05d2d285c6a47d4d606e0d2e053f0c0b1b7 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 4 May 2022 16:52:50 +0200 Subject: [PATCH 7/9] chore: wait for message to be dispatched Signed-off-by: blam --- plugins/scaffolder-backend/src/service/router.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 3f3cf1f4d7..6f6638e61a 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -229,6 +229,8 @@ describe('createRouter', () => { }, }); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(broker).toHaveBeenCalledWith( expect.objectContaining({ createdBy: 'user:default/guest', @@ -279,6 +281,8 @@ describe('createRouter', () => { }, }); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(broker).toHaveBeenCalledWith( expect.objectContaining({ createdBy: undefined, From 5af8f5b1fe84c29747296fe9ec5a102d04d16b95 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 5 May 2022 11:30:17 +0200 Subject: [PATCH 8/9] chore: add some debugging for CI Signed-off-by: blam --- .../src/service/router.test.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 6f6638e61a..30ea4d919e 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -216,18 +216,22 @@ describe('createRouter', () => { const mockToken = 'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob'; - await request(app) - .post('/v2/tasks') - .set('Authorization', `Bearer ${mockToken}`) - .send({ - templateRef: stringifyEntityRef({ - kind: 'template', - name: 'create-react-app-template', - }), - values: { - required: 'required-value', - }, - }); + console.log( + JSON.stringify( + await request(app) + .post('/v2/tasks') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }), + ), + ); await new Promise(resolve => setTimeout(resolve, 1000)); From 36441d992799f33b868ab82a495bd53242c063cb Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 5 May 2022 12:36:25 +0200 Subject: [PATCH 9/9] chore: fix the tests for node 14 Signed-off-by: blam --- plugins/scaffolder-backend/src/service/router.test.ts | 4 ---- plugins/scaffolder-backend/src/service/router.ts | 4 +++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 30ea4d919e..14c543249e 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -233,8 +233,6 @@ describe('createRouter', () => { ), ); - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(broker).toHaveBeenCalledWith( expect.objectContaining({ createdBy: 'user:default/guest', @@ -285,8 +283,6 @@ describe('createRouter', () => { }, }); - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(broker).toHaveBeenCalledWith( expect.objectContaining({ createdBy: undefined, diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 5a6690edab..5453dcd234 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -342,7 +342,9 @@ function parseBearerToken(header?: string): { if (!token) return {}; const [_header, rawPayload, _signature] = token.split('.'); - const payload: { sub: string } = JSON.parse(atob(rawPayload)); + const payload: { sub: string } = JSON.parse( + Buffer.from(rawPayload, 'base64').toString(), + ); return { entityRef: payload.sub,