diff --git a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts index 2532293acf..7ae4b6b5a0 100644 --- a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts @@ -23,7 +23,6 @@ import { SystemEntity, UserEntity, } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; import { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; describe('BuiltinKindsEntityProcessor', () => { @@ -539,38 +538,5 @@ describe('BuiltinKindsEntityProcessor', () => { }, }); }); - it('generates relations for template entities', async () => { - const entity: TemplateEntityV1beta2 = { - apiVersion: 'backstage.io/v1beta2', - kind: 'Template', - metadata: { name: 'n' }, - spec: { - parameters: {}, - steps: [], - type: 'service', - owner: 'o', - }, - }; - - await processor.postProcessEntity(entity, location, emit); - - expect(emit).toBeCalledTimes(2); - expect(emit).toBeCalledWith({ - type: 'relation', - relation: { - source: { kind: 'Group', namespace: 'default', name: 'o' }, - type: 'ownerOf', - target: { kind: 'Template', namespace: 'default', name: 'n' }, - }, - }); - expect(emit).toBeCalledWith({ - type: 'relation', - relation: { - source: { kind: 'Template', namespace: 'default', name: 'n' }, - type: 'ownedBy', - target: { kind: 'Group', namespace: 'default', name: 'o' }, - }, - }); - }); }); }); diff --git a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts index 9e420b9d65..4e8cf3fef7 100644 --- a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts @@ -48,10 +48,6 @@ import { UserEntity, userEntityV1alpha1Validator, } from '@backstage/catalog-model'; -import { - TemplateEntityV1beta2, - templateEntityV1beta2Validator, -} from '@backstage/plugin-scaffolder-common'; import { CatalogProcessor, CatalogProcessorEmit, @@ -67,7 +63,6 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { resourceEntityV1alpha1Validator, groupEntityV1alpha1Validator, locationEntityV1alpha1Validator, - templateEntityV1beta2Validator, userEntityV1alpha1Validator, systemEntityV1alpha1Validator, domainEntityV1alpha1Validator, @@ -135,19 +130,6 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { } } - /* - * Emit relations for the Template kind - */ - if (entity.kind === 'Template') { - const template = entity as TemplateEntityV1beta2; - doEmit( - template.spec.owner, - { defaultKind: 'Group', defaultNamespace: selfRef.namespace }, - RELATION_OWNED_BY, - RELATION_OWNER_OF, - ); - } - /* * Emit relations for the Component kind */ diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts index cdc2ce2150..ba31e21122 100644 --- a/plugins/catalog-react/src/filters.test.ts +++ b/plugins/catalog-react/src/filters.test.ts @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { EntityTextFilter } from './filters'; const entities: Entity[] = [ @@ -37,9 +37,9 @@ const entities: Entity[] = [ }, ]; -const templates: TemplateEntityV1beta2[] = [ +const templates: TemplateEntityV1beta3[] = [ { - apiVersion: 'backstage.io/v1beta2', + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { name: 'react-app', @@ -52,7 +52,7 @@ const templates: TemplateEntityV1beta2[] = [ }, }, { - apiVersion: 'backstage.io/v1beta2', + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { name: 'gRPC service', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 55ef68423e..f73c4dce3f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -19,10 +19,7 @@ import { Writable } from 'stream'; import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; import { TaskSecrets } from '../tasks/types'; -import { - TemplateInfo, - TemplateMetadata, -} from '@backstage/plugin-scaffolder-common'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; /** * ActionContext is passed into scaffolder actions. @@ -47,10 +44,6 @@ export type ActionContext = { */ createTemporaryDirectory(): Promise; - /** - * @deprecated please use templateInfo instead - */ - metadata?: TemplateMetadata; templateInfo?: TemplateInfo; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts deleted file mode 100644 index dcad1fc5d0..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Copyright 2021 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 mockFs from 'mock-fs'; -import * as winston from 'winston'; -import { createTemplateAction, TemplateActionRegistry } from '../actions'; -import { ScmIntegrations } from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; -import { TaskContext } from './types'; -import { RepoSpec } from '../actions/builtin/publish/util'; -import { TaskSpec } from '@backstage/plugin-scaffolder-common'; - -describe('LegacyWorkflowRunner', () => { - let runner: HandlebarsWorkflowRunner; - const logger = getVoidLogger(); - let actionRegistry = new TemplateActionRegistry(); - let fakeActionHandler: jest.Mock; - - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - github: [{ host: 'github.com', token: 'token' }], - }, - }), - ); - - const createMockTaskWithSpec = (spec: TaskSpec): TaskContext => ({ - spec, - complete: async () => {}, - done: false, - emitLog: async () => {}, - getWorkspaceName: () => Promise.resolve('test-workspace'), - }); - - beforeEach(() => { - winston.format.simple(); // put logform the require cache before mocking fs - mockFs({ - '/tmp': mockFs.directory(), - }); - - actionRegistry = new TemplateActionRegistry(); - actionRegistry.register({ - id: 'test-action', - handler: async ctx => { - ctx.output('testOutput', 'mockOutputData'); - ctx.output('badOutput', false); - }, - }); - fakeActionHandler = jest.fn(); - actionRegistry.register({ - id: 'test-metadata-action', - handler: fakeActionHandler, - }); - - runner = new HandlebarsWorkflowRunner({ - actionRegistry, - integrations, - workingDirectory: '/tmp', - logger, - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it('should fail when the action does not exist', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }); - - await expect(() => runner.execute(task)).rejects.toThrow( - /Template action with ID 'not-found-action' is not registered/, - ); - }); - - it('should pass metadata through', async () => { - const entityRef = `template:default/templateName`; - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { - id: 'test', - name: 'name', - action: 'test-metadata-action', - input: { foo: 1 }, - }, - ], - output: {}, - values: {}, - templateInfo: { entityRef }, - }); - - await runner.execute(task); - - expect(fakeActionHandler.mock.calls[0][0].templateInfo).toEqual({ - entityRef, - }); - }); - - describe('templating', () => { - it('should template the output', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'test-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - - it('should template the input', async () => { - const inputAction = createTemplateAction<{ - name: string; - }>({ - id: 'test-input', - schema: { - input: { - type: 'object', - required: ['name'], - properties: { - name: { - title: 'name', - description: 'Enter name', - type: 'string', - }, - }, - }, - }, - async handler(ctx) { - if (ctx.input.name !== 'mockOutputData') { - throw new Error( - `expected name to be "mockOutputData" got ${ctx.input.name}`, - ); - } - }, - }); - actionRegistry.register(inputAction); - - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'test-input', - name: 'test-input', - action: 'test-input', - input: { - name: '{{ steps.test.output.testOutput }}', - }, - }, - ], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - }); - - describe('conditionals', () => { - it('should execute steps conditionally', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'conditional', - name: 'conditional', - action: 'test-action', - if: '{{ steps.test.output.testOutput }}', - }, - ], - output: { - result: '{{ steps.conditional.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - - it('should execute steps conditionally with eq helper', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'conditional', - name: 'conditional', - action: 'test-action', - if: '{{ eq steps.test.output.testOutput "mockOutputData" }}', - }, - ], - output: { - result: '{{ steps.conditional.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - - it('should skip test conditionally', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'conditional', - name: 'conditional', - action: 'test-action', - if: '{{ steps.test.output.badOutput }}', - }, - ], - output: { - result: '{{ steps.conditional.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBeUndefined(); - }); - }); - - describe('parsing', () => { - it('should parse strings as objects if possible', async () => { - const inputAction = createTemplateAction<{ - address: { line1: string }; - list: string[]; - address2: string; - }>({ - id: 'test-input', - schema: { - input: { - type: 'object', - required: ['address'], - properties: { - address: { - title: 'address', - description: 'Enter name', - type: 'object', - properties: { - line1: { - type: 'string', - }, - }, - }, - address2: { - type: 'string', - }, - list: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - }, - }, - async handler(ctx) { - if (ctx.input.list.length !== 1) { - throw new Error( - `expected list to have length "1" got ${ctx.input.list.length}`, - ); - } - if (ctx.input.address.line1 !== 'line 1') { - throw new Error( - `expected address.line1 to be "line 1" got ${ctx.input.address.line1}`, - ); - } - - if (ctx.input.address2 !== '{"not valid"}') { - throw new Error( - `expected address2 to be "{"not valid"}" got ${ctx.input.address2}`, - ); - } - ctx.output('address', ctx.input.address.line1); - }, - }); - actionRegistry.register(inputAction); - - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { - id: 'test-input', - name: 'test-input', - action: 'test-input', - input: { - address: JSON.stringify({ line1: 'line 1' }), - list: JSON.stringify(['hey!']), - address2: '{"not valid"}', - }, - }, - ], - output: { - result: '{{ steps.test-input.output.address }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('line 1'); - }); - - it('should provide a parseRepoUrl helper', async () => { - const inputAction = createTemplateAction<{ - destination: RepoSpec; - }>({ - id: 'test-input', - schema: { - input: { - type: 'object', - required: ['destination'], - properties: { - destination: { - title: 'destination', - type: 'object', - properties: { - repo: { - type: 'string', - }, - host: { - type: 'string', - }, - owner: { - type: 'string', - }, - organization: { - type: 'string', - }, - workspace: { - type: 'string', - }, - project: { - type: 'string', - }, - }, - }, - }, - }, - }, - async handler(ctx) { - ctx.output('host', ctx.input.destination.host); - ctx.output('repo', ctx.input.destination.repo); - - if (ctx.input.destination.owner) { - ctx.output('owner', ctx.input.destination.owner); - } - - if (ctx.input.destination.host !== 'github.com') { - throw new Error( - `expected host to be "github.com" got ${ctx.input.destination.host}`, - ); - } - - if (ctx.input.destination.repo !== 'repo') { - throw new Error( - `expected repo to be "repo" got ${ctx.input.destination.repo}`, - ); - } - - if ( - ctx.input.destination.owner && - ctx.input.destination.owner !== 'owner' - ) { - throw new Error( - `expected repo to be "owner" got ${ctx.input.destination.owner}`, - ); - } - }, - }); - actionRegistry.register(inputAction); - - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { - id: 'test-input', - name: 'test-input', - action: 'test-input', - input: { - destination: '{{ parseRepoUrl parameters.repoUrl }}', - }, - }, - ], - output: { - host: '{{ steps.test-input.output.host }}', - repo: '{{ steps.test-input.output.repo }}', - owner: '{{ steps.test-input.output.owner }}', - }, - values: { - repoUrl: 'github.com?repo=repo&owner=owner', - }, - }); - - const { output } = await runner.execute(task); - - expect(output.host).toBe('github.com'); - expect(output.repo).toBe('repo'); - expect(output.owner).toBe('owner'); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts deleted file mode 100644 index c82da51ead..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright 2021 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 { TaskContext, WorkflowRunner, WorkflowResponse } from './types'; -import * as Handlebars from 'handlebars'; -import { TemplateActionRegistry } from '..'; -import { ScmIntegrations } from '@backstage/integration'; -import { parseRepoUrl } from '../actions/builtin/publish/util'; -import { isTruthy } from './helper'; -import { PassThrough } from 'stream'; -import * as winston from 'winston'; -import { Logger } from 'winston'; -import path from 'path'; -import fs from 'fs-extra'; -import { validate as validateJsonSchema } from 'jsonschema'; -import { JsonObject, JsonValue } from '@backstage/types'; -import { InputError } from '@backstage/errors'; -import { TaskSpec, TaskSpecV1beta2 } from '@backstage/plugin-scaffolder-common'; - -type Options = { - workingDirectory: string; - actionRegistry: TemplateActionRegistry; - integrations: ScmIntegrations; - logger: Logger; -}; - -const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta2 => - taskSpec.apiVersion === 'backstage.io/v1beta2'; -/** - * This is the legacy workflow runner, which supports handlebars. This entire implementation will be replaced - * with the default workflow runner interface in the future so this entire thing can go bye bye. - */ -export class HandlebarsWorkflowRunner implements WorkflowRunner { - private readonly handlebars: typeof Handlebars; - - constructor(private readonly options: Options) { - this.handlebars = Handlebars.create(); - - // TODO(blam): this should be a public facing API but it's a little - // scary right now, so we're going to lock it off like the component API is - // in the frontend until we can work out a nice way to do it. - this.handlebars.registerHelper('parseRepoUrl', repoUrl => { - return JSON.stringify(parseRepoUrl(repoUrl, this.options.integrations)); - }); - - this.handlebars.registerHelper('projectSlug', repoUrl => { - const { owner, repo } = parseRepoUrl(repoUrl, this.options.integrations); - return `${owner}/${repo}`; - }); - - this.handlebars.registerHelper('json', obj => JSON.stringify(obj)); - - this.handlebars.registerHelper('not', value => !isTruthy(value)); - - this.handlebars.registerHelper('eq', (a, b) => a === b); - } - - async execute(task: TaskContext): Promise { - if (!isValidTaskSpec(task.spec)) { - throw new InputError(`Task spec is not a valid v1beta2 task spec`); - } - - const { actionRegistry } = this.options; - - const workspacePath = path.join( - this.options.workingDirectory, - await task.getWorkspaceName(), - ); - try { - await fs.ensureDir(workspacePath); - await task.emitLog( - `Starting up task with ${task.spec.steps.length} steps`, - ); - - const templateCtx: { - parameters: JsonObject; - steps: { - [stepName: string]: { output: { [outputName: string]: JsonValue } }; - }; - } = { parameters: task.spec.values, steps: {} }; - - for (const step of task.spec.steps) { - const metadata = { stepId: step.id }; - try { - const taskLogger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'info', - format: winston.format.combine( - winston.format.colorize(), - winston.format.timestamp(), - winston.format.simple(), - ), - defaultMeta: {}, - }); - - const stream = new PassThrough(); - stream.on('data', async data => { - const message = data.toString().trim(); - if (message?.length > 1) { - await task.emitLog(message, metadata); - } - }); - - taskLogger.add(new winston.transports.Stream({ stream })); - - if (step.if !== undefined) { - // Support passing values like false to disable steps - let skip = !step.if; - - // Evaluate strings as handlebar templates - if (typeof step.if === 'string') { - const condition = JSON.parse( - JSON.stringify(step.if), - (_key, value) => { - if (typeof value === 'string') { - const templated = this.handlebars.compile(value, { - noEscape: true, - data: false, - preventIndent: true, - })(templateCtx); - - // If it's just an empty string, treat it as undefined - if (templated === '') { - return undefined; - } - - try { - return JSON.parse(templated); - } catch { - return templated; - } - } - - return value; - }, - ); - - skip = !isTruthy(condition); - } - - if (skip) { - await task.emitLog(`Skipped step ${step.name}`, { - ...metadata, - status: 'skipped', - }); - continue; - } - } - - await task.emitLog(`Beginning step ${step.name}`, { - ...metadata, - status: 'processing', - }); - - const action = actionRegistry.get(step.action); - if (!action) { - throw new Error(`Action '${step.action}' does not exist`); - } - - const input = - step.input && - JSON.parse(JSON.stringify(step.input), (_key, value) => { - if (typeof value === 'string') { - const templated = this.handlebars.compile(value, { - noEscape: true, - data: false, - preventIndent: true, - })(templateCtx); - - // If it smells like a JSON object then give it a parse as an object and if it fails return the string - if ( - (templated.startsWith('"') && templated.endsWith('"')) || - (templated.startsWith('{') && templated.endsWith('}')) || - (templated.startsWith('[') && templated.endsWith(']')) - ) { - try { - // Don't recursively JSON parse the values of this string. - // Shouldn't need to, don't want to encourage the use of returning handlebars from somewhere else - return JSON.parse(templated); - } catch { - return templated; - } - } - return templated; - } - - return value; - }); - - if (action.schema?.input) { - const validateResult = validateJsonSchema( - input, - action.schema.input, - ); - if (!validateResult.valid) { - const errors = validateResult.errors.join(', '); - throw new InputError( - `Invalid input passed to action ${action.id}, ${errors}`, - ); - } - } - - const stepOutputs: { [name: string]: JsonValue } = {}; - - // Keep track of all tmp dirs that are created by the action so we can remove them after - const tmpDirs = new Array(); - - this.options.logger.debug(`Running ${action.id} with input`, { - input: JSON.stringify(input, null, 2), - }); - - await action.handler({ - // deprecated in favor of templateInfo.baseUrl - baseUrl: task.spec.baseUrl, - logger: taskLogger, - logStream: stream, - input, - secrets: task.secrets ?? {}, - workspacePath, - async createTemporaryDirectory() { - const tmpDir = await fs.mkdtemp( - `${workspacePath}_step-${step.id}-`, - ); - tmpDirs.push(tmpDir); - return tmpDir; - }, - output(name: string, value: JsonValue) { - stepOutputs[name] = value; - }, - // deprecated in favor of templateInfo - metadata: task.spec.metadata, - templateInfo: task.spec.templateInfo, - }); - - // Remove all temporary directories that were created when executing the action - for (const tmpDir of tmpDirs) { - await fs.remove(tmpDir); - } - - templateCtx.steps[step.id] = { output: stepOutputs }; - - await task.emitLog(`Finished step ${step.name}`, { - ...metadata, - status: 'completed', - }); - } catch (error) { - await task.emitLog(String(error.stack), { - ...metadata, - status: 'failed', - }); - throw error; - } - } - - const output = JSON.parse( - JSON.stringify(task.spec.output), - (_key, value) => { - if (typeof value === 'string') { - const templated = this.handlebars.compile(value, { - noEscape: true, - data: false, - preventIndent: true, - })(templateCtx); - - // If it's just an empty string, treat it as undefined - if (templated === '') { - return undefined; - } - - // If it smells like a JSON object then give it a parse as an object and if it fails return the string - if ( - (templated.startsWith('"') && templated.endsWith('"')) || - (templated.startsWith('{') && templated.endsWith('}')) || - (templated.startsWith('[') && templated.endsWith(']')) - ) { - try { - // Don't recursively JSON parse the values of this string. - // Shouldn't need to, don't want to encourage the use of returning handlebars from somewhere else - return JSON.parse(templated); - } catch { - return templated; - } - } - return templated; - } - return value; - }, - ); - - return { output }; - } finally { - if (workspacePath) { - await fs.remove(workspacePath); - } - } - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index fb9da56c22..0bd5a3c840 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -257,8 +257,6 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const stepOutput: { [outputName: string]: JsonValue } = {}; await action.handler({ - // deprecated in favourof templateInfo.baseUrl - baseUrl: task.spec.baseUrl, input, secrets: task.secrets ?? {}, logger: taskLogger, @@ -274,8 +272,6 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { output(name: string, value: JsonValue) { stepOutput[name] = value; }, - // deprecated in favour of templateInfo - metadata: task.spec.metadata, templateInfo: task.spec.templateInfo, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 9ff6b9c8a1..1a099e4c53 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -19,16 +19,10 @@ import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker } from './StorageTaskBroker'; import { TaskWorker } from './TaskWorker'; -import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateActionRegistry } from '../actions'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; -jest.mock('./HandlebarsWorkflowRunner'); -const MockedHandlebarsWorkflowRunner = - HandlebarsWorkflowRunner as jest.Mock; -MockedHandlebarsWorkflowRunner.mockImplementation(); - jest.mock('./NunjucksWorkflowRunner'); const MockedNunjucksWorkflowRunner = NunjucksWorkflowRunner as jest.Mock; @@ -58,10 +52,6 @@ describe('TaskWorker', () => { const actionRegistry: TemplateActionRegistry = {} as TemplateActionRegistry; const workingDirectory = '/tmp/scaffolder'; - const handlebarsWorkflowRunner: HandlebarsWorkflowRunner = { - execute: jest.fn(), - } as unknown as HandlebarsWorkflowRunner; - const workflowRunner: NunjucksWorkflowRunner = { execute: jest.fn(), } as unknown as NunjucksWorkflowRunner; @@ -72,47 +62,11 @@ describe('TaskWorker', () => { beforeEach(() => { jest.resetAllMocks(); - MockedHandlebarsWorkflowRunner.mockImplementation( - () => handlebarsWorkflowRunner, - ); MockedNunjucksWorkflowRunner.mockImplementation(() => workflowRunner); }); const logger = getVoidLogger(); - it('should call the legacy workflow runner when the apiVersion is not beta3', async () => { - const broker = new StorageTaskBroker(storage, logger); - const taskWorker = await TaskWorker.create({ - logger, - workingDirectory, - integrations, - taskBroker: broker, - actionRegistry, - }); - - await broker.dispatch({ - spec: { - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }, - }); - - const task = await broker.claim(); - await taskWorker.runOneTask(task); - - expect(MockedHandlebarsWorkflowRunner).toBeCalledWith({ - actionRegistry, - integrations, - logger, - workingDirectory, - }); - expect(handlebarsWorkflowRunner.execute).toHaveBeenCalled(); - }); - it('should call the default workflow runner when the apiVersion is beta3', async () => { const broker = new StorageTaskBroker(storage, logger); const taskWorker = await TaskWorker.create({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 8de9359806..9defd71c49 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -15,7 +15,6 @@ */ import { TaskContext, TaskBroker, WorkflowRunner } from './types'; -import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { Logger } from 'winston'; import { TemplateActionRegistry } from '../actions'; @@ -31,7 +30,6 @@ import { TemplateFilter } from '../../lib/templating/SecureTemplater'; export type TaskWorkerOptions = { taskBroker: TaskBroker; runners: { - legacyWorkflowRunner: HandlebarsWorkflowRunner; workflowRunner: WorkflowRunner; }; }; @@ -68,13 +66,6 @@ export class TaskWorker { additionalTemplateFilters, } = options; - const legacyWorkflowRunner = new HandlebarsWorkflowRunner({ - logger, - actionRegistry, - integrations, - workingDirectory, - }); - const workflowRunner = new NunjucksWorkflowRunner({ actionRegistry, integrations, @@ -85,7 +76,7 @@ export class TaskWorker { return new TaskWorker({ taskBroker: taskBroker, - runners: { legacyWorkflowRunner, workflowRunner }, + runners: { workflowRunner }, }); } @@ -100,10 +91,15 @@ export class TaskWorker { async runOneTask(task: TaskContext) { try { - const { output } = - task.spec.apiVersion === 'scaffolder.backstage.io/v1beta3' - ? await this.options.runners.workflowRunner.execute(task) - : await this.options.runners.legacyWorkflowRunner.execute(task); + if (task.spec.apiVersion !== 'scaffolder.backstage.io/v1beta3') { + throw new Error( + `Unsupported Template apiVersion ${task.spec.apiVersion}`, + ); + } + + const { output } = await this.options.runners.workflowRunner.execute( + task, + ); await task.complete('completed', { output }); } catch (error) { diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index a7148dac40..23200d6aac 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -26,10 +26,7 @@ import { } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, InputError, NotFoundError } from '@backstage/errors'; -import { - TemplateEntityV1beta2, - TemplateEntityV1beta3, -} from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import fs from 'fs-extra'; import os from 'os'; import { Logger } from 'winston'; @@ -95,7 +92,7 @@ export async function findTemplate(options: { entityRef: CompoundEntityRef; token?: string; catalogApi: CatalogApi; -}): Promise { +}): Promise { const { entityRef, token, catalogApi } = options; if (entityRef.namespace.toLocaleLowerCase('en-US') !== DEFAULT_NAMESPACE) { @@ -114,5 +111,5 @@ export async function findTemplate(options: { ); } - return template as TemplateEntityV1beta3 | TemplateEntityV1beta2; + return template as TemplateEntityV1beta3; } diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 1c57153fb0..41bc73c7ae 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -35,7 +35,7 @@ import { UrlReaders, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ConfigReader } from '@backstage/config'; import ObservableImpl from 'zen-observable'; import express from 'express'; @@ -76,8 +76,8 @@ const mockUrlReader = UrlReaders.default({ describe('createRouter', () => { let app: express.Express; let taskBroker: TaskBroker; - const template: TemplateEntityV1beta2 = { - apiVersion: 'backstage.io/v1beta2', + const template: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { description: 'Create a new CRA website project', diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index f7e375b0b2..cf073b551b 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -22,11 +22,8 @@ import { Config } from '@backstage/config'; import { InputError, NotFoundError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { - TemplateEntityV1beta2, TemplateEntityV1beta3, - TaskSpecV1beta3, TaskSpec, - TaskSpecV1beta2, } from '@backstage/plugin-scaffolder-common'; import express from 'express'; import Router from 'express-promise-router'; @@ -61,13 +58,8 @@ export interface RouterOptions { additionalTemplateFilters?: Record; } -function isSupportedTemplate( - entity: TemplateEntityV1beta2 | TemplateEntityV1beta3, -) { - return ( - entity.apiVersion === 'backstage.io/v1beta2' || - entity.apiVersion === 'scaffolder.backstage.io/v1beta3' - ); +function isSupportedTemplate(entity: TemplateEntityV1beta3) { + return entity.apiVersion === 'scaffolder.backstage.io/v1beta3'; } /** @public */ @@ -181,64 +173,7 @@ export async function createRouter( token: getBearerToken(req.headers.authorization), }); - let taskSpec: TaskSpec; - - if (isSupportedTemplate(template)) { - if (template.apiVersion === 'backstage.io/v1beta2') { - logger.warn( - `Scaffolding ${stringifyEntityRef( - template, - )} with deprecated apiVersion ${ - template.apiVersion - }. Please migrate the template to backstage.io/v1beta3. https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3`, - ); - } - - for (const parameters of [template.spec.parameters ?? []].flat()) { - const result = validate(values, parameters); - if (!result.valid) { - res.status(400).json({ errors: result.errors }); - return; - } - } - - const baseUrl = getEntityBaseUrl(template); - - const baseTaskSpec = { - baseUrl, - steps: template.spec.steps.map((step, index) => ({ - ...step, - id: step.id ?? `step-${index + 1}`, - name: step.name ?? step.action, - })), - output: template.spec.output ?? {}, - - // deprecated in favour of templateInfo - metadata: { name: template.metadata?.name }, - - templateInfo: { - entityRef: stringifyEntityRef({ - kind, - namespace, - name: template.metadata?.name, - }), - baseUrl, - }, - }; - - taskSpec = - template.apiVersion === 'backstage.io/v1beta2' - ? ({ - ...baseTaskSpec, - apiVersion: template.apiVersion, - values, - } as TaskSpecV1beta2) - : ({ - ...baseTaskSpec, - apiVersion: template.apiVersion, - parameters: values, - } as TaskSpecV1beta3); - } else { + if (!isSupportedTemplate(template)) { throw new InputError( `Unsupported apiVersion field in schema entity, ${ (template as Entity).apiVersion @@ -246,6 +181,35 @@ export async function createRouter( ); } + for (const parameters of [template.spec.parameters ?? []].flat()) { + const result = validate(values, parameters); + if (!result.valid) { + res.status(400).json({ errors: result.errors }); + return; + } + } + + const baseUrl = getEntityBaseUrl(template); + + const taskSpec: TaskSpec = { + apiVersion: template.apiVersion, + steps: template.spec.steps.map((step, index) => ({ + ...step, + id: step.id ?? `step-${index + 1}`, + name: step.name ?? step.action, + })), + output: template.spec.output ?? {}, + parameters: values, + templateInfo: { + entityRef: stringifyEntityRef({ + kind, + namespace, + name: template.metadata?.name, + }), + baseUrl, + }, + }; + const result = await taskBroker.dispatch({ spec: taskSpec, secrets: { diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index a456f2bdd3..b675fdb357 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -17,7 +17,7 @@ import React, { ComponentType } from 'react'; import { Routes, Route, useOutlet } from 'react-router'; import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; @@ -35,14 +35,14 @@ import { useElementFilter } from '@backstage/core-plugin-api'; export type RouterProps = { /** @deprecated use components.TemplateCardComponent instead */ TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; /** @deprecated use component.TaskPageComponent instead */ TaskPageComponent?: ComponentType<{}>; components?: { TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; TaskPageComponent?: ComponentType<{}>; }; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index da9f1f74b8..0b3fccc07c 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -24,7 +24,7 @@ import { SupportButton, } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { useRouteRef } from '@backstage/core-plugin-api'; import { EntityKindPicker, @@ -52,7 +52,7 @@ const useStyles = makeStyles(theme => ({ export type ScaffolderPageProps = { TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; groups?: Array<{ title?: React.ReactNode; diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index aab5e8348a..f73e32189c 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -298,10 +298,7 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => { return; } - const formData = - taskStream.task!.spec.apiVersion === 'backstage.io/v1beta2' - ? taskStream.task!.spec.values - : taskStream.task!.spec.parameters; + const formData = taskStream.task!.spec.parameters; const { name } = parseEntityRef( taskStream.task!.spec.templateInfo?.entityRef, diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 7275aa3a9e..54f8188e3b 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrationIcon, scmIntegrationsApiRef, @@ -99,7 +99,7 @@ const useDeprecationStyles = makeStyles(theme => ({ })); export type TemplateCardProps = { - template: TemplateEntityV1beta2; + template: TemplateEntityV1beta3; deprecated?: boolean; }; @@ -112,7 +112,7 @@ type TemplateProps = { }; const getTemplateCardProps = ( - template: TemplateEntityV1beta2, + template: TemplateEntityV1beta3, ): TemplateProps & { key: string } => { return { key: template.metadata.uid!, diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx index 3d9da542de..e3cf6eac05 100644 --- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx +++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx @@ -16,7 +16,7 @@ import React, { ComponentType } from 'react'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { Content, ContentHeader, @@ -34,7 +34,7 @@ import { TemplateCard } from '../TemplateCard'; */ export type TemplateListProps = { TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; group?: { title?: React.ReactNode; @@ -106,7 +106,7 @@ export const TemplateList = ({ maybeFilteredEntities.map((template: Entity) => ( ))}