chore: acutally make the scaffolder backend run the v3 template
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
@@ -12,13 +12,17 @@ spec:
|
||||
properties:
|
||||
inputString:
|
||||
type: string
|
||||
title: string input test
|
||||
inputObject:
|
||||
type: object
|
||||
title: object input test
|
||||
properties:
|
||||
first:
|
||||
type: string
|
||||
title: first
|
||||
second:
|
||||
type: number
|
||||
title: second
|
||||
steps:
|
||||
- id: debug
|
||||
name: Debug
|
||||
|
||||
@@ -55,6 +55,14 @@ describe('DefaultWorkflowRunner', () => {
|
||||
handler: fakeActionHandler,
|
||||
});
|
||||
|
||||
actionRegistry.register({
|
||||
id: 'output-action',
|
||||
description: 'Mock action for testing',
|
||||
handler: async ctx => {
|
||||
ctx.output('mock', 'backstage');
|
||||
},
|
||||
});
|
||||
|
||||
runner = new DefaultWorkflowRunner({
|
||||
actionRegistry,
|
||||
integrations,
|
||||
@@ -189,5 +197,27 @@ describe('DefaultWorkflowRunner', () => {
|
||||
expect.objectContaining({ input: { foo: 1 } }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should template the output from simple actions', async () => {
|
||||
const task = createMockTaskWithSpec({
|
||||
apiVersion: 'backstage.io/v1beta3',
|
||||
steps: [
|
||||
{
|
||||
id: 'test',
|
||||
name: 'name',
|
||||
action: 'output-action',
|
||||
input: {},
|
||||
},
|
||||
],
|
||||
output: {
|
||||
foo: '${{steps.test.output.mock | upper}}',
|
||||
},
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const { output } = await runner.execute(task);
|
||||
|
||||
expect(output.foo).toEqual('BACKSTAGE');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,6 +87,43 @@ export class DefaultWorkflowRunner implements WorkflowRunner {
|
||||
});
|
||||
}
|
||||
|
||||
private render<T>(input: T, context: TemplateContext): T {
|
||||
return JSON.parse(JSON.stringify(input), (_key, value) => {
|
||||
try {
|
||||
if (typeof value === 'string') {
|
||||
const templated = this.nunjucks.renderString(value, context);
|
||||
try {
|
||||
return JSON.parse(templated);
|
||||
} catch {
|
||||
return templated;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
private makeStringifyableParams<T>(input: T): T {
|
||||
/**
|
||||
* This is a little bit of a hack / magic so that when we use nunjucks and we try to
|
||||
* pass through something other than a string from the parameters section.
|
||||
* When an accessor is used that is an object, it's toString is the JSON.stringify'd version of it's children
|
||||
* Which makes it work really well in string templating as we can parse the result again after.yarn
|
||||
*/
|
||||
return JSON.parse(
|
||||
JSON.stringify(input),
|
||||
(key: string, value: JsonObject) => {
|
||||
if (typeof value === 'object' && key) {
|
||||
value.toString = () => JSON.stringify(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async execute(task: Task): Promise<WorkflowResponse> {
|
||||
if (!isValidTaskSpec(task.spec)) {
|
||||
throw new InputError(
|
||||
@@ -103,70 +140,61 @@ export class DefaultWorkflowRunner implements WorkflowRunner {
|
||||
`Starting up task with ${task.spec.steps.length} steps`,
|
||||
);
|
||||
|
||||
/**
|
||||
* This is a little bit of a hack / magic so that when we use nunjucks and we try to
|
||||
* pass through an object from the `parameters` section of the task spec, it will
|
||||
* actually work as the toString method is called from the nunjucks template.
|
||||
*/
|
||||
const parsedParams = JSON.parse(
|
||||
JSON.stringify(task.spec.parameters),
|
||||
(key: string, value: JsonObject) => {
|
||||
if (typeof value === 'object' && key) {
|
||||
value.toString = () => JSON.stringify(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
);
|
||||
|
||||
const context: TemplateContext = {
|
||||
parameters: parsedParams,
|
||||
parameters: this.makeStringifyableParams(task.spec.parameters),
|
||||
steps: {},
|
||||
};
|
||||
|
||||
for (const step of task.spec.steps) {
|
||||
const action = this.options.actionRegistry.get(step.action);
|
||||
const { taskLogger, streamLogger } = createStepLogger({ task, step });
|
||||
try {
|
||||
const action = this.options.actionRegistry.get(step.action);
|
||||
const { taskLogger, streamLogger } = createStepLogger({ task, step });
|
||||
|
||||
const input =
|
||||
step.input &&
|
||||
JSON.parse(JSON.stringify(step.input), (_key, value) => {
|
||||
try {
|
||||
if (typeof value === 'string') {
|
||||
const templated = this.nunjucks.renderString(value, context);
|
||||
try {
|
||||
return JSON.parse(templated);
|
||||
} catch {
|
||||
return templated;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
const input = step.input && this.render(step.input, context);
|
||||
|
||||
const tmpDirs = new Array<string>();
|
||||
const stepOutput: { [outputName: string]: JsonValue } = {};
|
||||
|
||||
await task.emitLog(`Beginning step ${step.name}`, {
|
||||
stepId: step.id,
|
||||
status: 'processing',
|
||||
});
|
||||
|
||||
const tmpDirs = new Array<string>();
|
||||
const stepOutputs: { [outputName: string]: JsonValue } = {};
|
||||
await action.handler({
|
||||
baseUrl: task.spec.baseUrl,
|
||||
input,
|
||||
logger: taskLogger,
|
||||
logStream: streamLogger,
|
||||
workspacePath,
|
||||
createTemporaryDirectory: async () => {
|
||||
const tmpDir = await fs.mkdtemp(
|
||||
`${workspacePath}_step-${step.id}-`,
|
||||
);
|
||||
tmpDirs.push(tmpDir);
|
||||
return tmpDir;
|
||||
},
|
||||
output(name: string, value: JsonValue) {
|
||||
stepOutput[name] = value;
|
||||
},
|
||||
});
|
||||
|
||||
await action.handler({
|
||||
baseUrl: task.spec.baseUrl,
|
||||
input,
|
||||
logger: taskLogger,
|
||||
logStream: streamLogger,
|
||||
workspacePath,
|
||||
createTemporaryDirectory: async () => {
|
||||
const tmpDir = await fs.mkdtemp(
|
||||
`${workspacePath}_step-${step.id}-`,
|
||||
);
|
||||
tmpDirs.push(tmpDir);
|
||||
return tmpDir;
|
||||
},
|
||||
output(name: string, value: JsonValue) {
|
||||
stepOutputs[name] = value;
|
||||
},
|
||||
});
|
||||
context.steps[step.id] = { output: stepOutput };
|
||||
|
||||
await task.emitLog(`Finished step ${step.name}`, {
|
||||
stepId: step.id,
|
||||
status: 'completed',
|
||||
});
|
||||
} catch (err) {
|
||||
await task.emitLog(String(err.stack), {
|
||||
stepId: step.id,
|
||||
status: 'failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const output = this.render(task.spec.output, context);
|
||||
|
||||
return { output };
|
||||
} finally {
|
||||
if (workspacePath) {
|
||||
await fs.remove(workspacePath);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
WorkflowRunner,
|
||||
WorkflowResponse,
|
||||
TaskSpecV1beta2,
|
||||
TaskSpec,
|
||||
} from './types';
|
||||
import * as Handlebars from 'handlebars';
|
||||
import { TemplateActionRegistry } from '..';
|
||||
@@ -75,6 +76,7 @@ export class LegacyWorkflowRunner implements WorkflowRunner {
|
||||
if (!isValidTaskSpec(task.spec)) {
|
||||
throw new InputError(`Task spec is not a valid v1beta2 task spec`);
|
||||
}
|
||||
|
||||
const { actionRegistry } = this.options;
|
||||
|
||||
const workspacePath = path.join(
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('TaskWorker', () => {
|
||||
output: {
|
||||
result: '{{ steps.test.output.testOutput }}',
|
||||
},
|
||||
values: {},
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const task = await broker.claim();
|
||||
@@ -130,7 +130,7 @@ describe('TaskWorker', () => {
|
||||
output: {
|
||||
result: '{{ steps.test.output.testOutput }}',
|
||||
},
|
||||
values: {},
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const task = await broker.claim();
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TemplateAction } from '../scaffolder/actions';
|
||||
import { createBuiltinActions } from '../scaffolder/actions/builtin/createBuiltinActions';
|
||||
import { LegacyWorkflowRunner } from '../scaffolder/tasks/LegacyWorkflowRunner';
|
||||
import { DefaultWorkflowRunner } from '../scaffolder/tasks/DefaultWorkflowRunner';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
@@ -90,14 +92,28 @@ export async function createRouter(
|
||||
);
|
||||
const taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
|
||||
const actionRegistry = new TemplateActionRegistry();
|
||||
const legacyWorkflowRunner = new LegacyWorkflowRunner({
|
||||
logger,
|
||||
actionRegistry,
|
||||
integrations,
|
||||
workingDirectory,
|
||||
});
|
||||
|
||||
const workflowRunner = new DefaultWorkflowRunner({
|
||||
actionRegistry,
|
||||
integrations,
|
||||
logger,
|
||||
workingDirectory,
|
||||
});
|
||||
|
||||
const workers = [];
|
||||
for (let i = 0; i < (taskWorkers || 1); i++) {
|
||||
const worker = new TaskWorker({
|
||||
logger,
|
||||
taskBroker,
|
||||
actionRegistry,
|
||||
workingDirectory,
|
||||
integrations,
|
||||
runners: {
|
||||
legacyWorkflowRunner,
|
||||
workflowRunner,
|
||||
},
|
||||
});
|
||||
workers.push(worker);
|
||||
}
|
||||
@@ -184,18 +200,32 @@ export async function createRouter(
|
||||
}
|
||||
|
||||
const baseUrl = getEntityBaseUrl(template);
|
||||
|
||||
taskSpec = {
|
||||
apiVersion: template.apiVersion,
|
||||
baseUrl,
|
||||
values,
|
||||
steps: template.spec.steps.map((step, index) => ({
|
||||
...step,
|
||||
id: step.id ?? `step-${index + 1}`,
|
||||
name: step.name ?? step.action,
|
||||
})),
|
||||
output: template.spec.output ?? {},
|
||||
};
|
||||
// TODO: need to make sure that the TaskSpec is the right format here.
|
||||
// If it's beta2 use values, beta3 uses parameters to clear that up.
|
||||
taskSpec =
|
||||
template.apiVersion === 'backstage.io/v1beta2'
|
||||
? {
|
||||
apiVersion: template.apiVersion,
|
||||
baseUrl,
|
||||
values,
|
||||
steps: template.spec.steps.map((step, index) => ({
|
||||
...step,
|
||||
id: step.id ?? `step-${index + 1}`,
|
||||
name: step.name ?? step.action,
|
||||
})),
|
||||
output: template.spec.output ?? {},
|
||||
}
|
||||
: {
|
||||
apiVersion: template.apiVersion,
|
||||
baseUrl,
|
||||
parameters: values,
|
||||
steps: template.spec.steps.map((step, index) => ({
|
||||
...step,
|
||||
id: step.id ?? `step-${index + 1}`,
|
||||
name: step.name ?? step.action,
|
||||
})),
|
||||
output: template.spec.output ?? {},
|
||||
};
|
||||
} else {
|
||||
throw new InputError(
|
||||
`Unsupported apiVersion field in schema entity, ${
|
||||
|
||||
Reference in New Issue
Block a user