Implement conditional steps in backend
Signed-off-by: Oliver Sand <oliver.sand@sda-se.com>
This commit is contained in:
@@ -18,13 +18,13 @@ import {
|
||||
getVoidLogger,
|
||||
SingleConnectionDatabaseManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { TaskWorker } from './TaskWorker';
|
||||
import os from 'os';
|
||||
import { ConfigReader, JsonObject } from '@backstage/config';
|
||||
import { StorageTaskBroker } from './StorageTaskBroker';
|
||||
import { DatabaseTaskStore } from './DatabaseTaskStore';
|
||||
import os from 'os';
|
||||
import { createTemplateAction, TemplateActionRegistry } from '../actions';
|
||||
import { RepoSpec } from '../actions/builtin/publish/util';
|
||||
import { DatabaseTaskStore } from './DatabaseTaskStore';
|
||||
import { StorageTaskBroker } from './StorageTaskBroker';
|
||||
import { TaskWorker } from './TaskWorker';
|
||||
|
||||
async function createStore(): Promise<DatabaseTaskStore> {
|
||||
const manager = SingleConnectionDatabaseManager.fromConfig(
|
||||
@@ -54,6 +54,7 @@ describe('TaskWorker', () => {
|
||||
id: 'test-action',
|
||||
handler: async ctx => {
|
||||
ctx.output('testOutput', 'winning');
|
||||
ctx.output('badOutput', false);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -172,6 +173,74 @@ describe('TaskWorker', () => {
|
||||
expect((event?.body?.output as JsonObject).result).toBe('winning');
|
||||
});
|
||||
|
||||
it('should execute steps conditionally', async () => {
|
||||
const broker = new StorageTaskBroker(storage, logger);
|
||||
const taskWorker = new TaskWorker({
|
||||
logger,
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
});
|
||||
|
||||
const { taskId } = await broker.dispatch({
|
||||
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 task = await broker.claim();
|
||||
await taskWorker.runOneTask(task);
|
||||
|
||||
const { events } = await storage.listEvents({ taskId });
|
||||
const event = events.find(e => e.type === 'completion');
|
||||
console.warn(events);
|
||||
expect((event?.body?.output as JsonObject).result).toBe('winning');
|
||||
});
|
||||
|
||||
it('should skip steps conditionally', async () => {
|
||||
const broker = new StorageTaskBroker(storage, logger);
|
||||
const taskWorker = new TaskWorker({
|
||||
logger,
|
||||
workingDirectory: os.tmpdir(),
|
||||
actionRegistry,
|
||||
taskBroker: broker,
|
||||
});
|
||||
|
||||
const { taskId } = await broker.dispatch({
|
||||
steps: [
|
||||
{ id: 'test', name: 'test', action: 'test-action' },
|
||||
{
|
||||
id: 'conditional',
|
||||
name: 'conditional',
|
||||
action: 'test-action',
|
||||
if: '{{ steps.test.output.badOutput }}',
|
||||
},
|
||||
],
|
||||
output: {
|
||||
result:
|
||||
'{{#if steps.conditional}}{{ steps.conditional.output.testOutput }}{{/if}}',
|
||||
},
|
||||
values: {},
|
||||
});
|
||||
|
||||
const task = await broker.claim();
|
||||
await taskWorker.runOneTask(task);
|
||||
|
||||
const { events } = await storage.listEvents({ taskId });
|
||||
const event = events.find(e => e.type === 'completion');
|
||||
expect((event?.body?.output as JsonObject).result).toEqual('');
|
||||
});
|
||||
|
||||
it('should parse strings as objects if possible', async () => {
|
||||
const inputAction = createTemplateAction<{
|
||||
address: { line1: string };
|
||||
|
||||
@@ -14,18 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PassThrough } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
import * as winston from 'winston';
|
||||
import { JsonValue, JsonObject } from '@backstage/config';
|
||||
import { validate as validateJsonSchema } from 'jsonschema';
|
||||
import { TaskBroker, Task } from './types';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { TemplateActionRegistry } from '../actions/TemplateActionRegistry';
|
||||
import * as Handlebars from 'handlebars';
|
||||
import { JsonObject, JsonValue } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import fs from 'fs-extra';
|
||||
import * as Handlebars from 'handlebars';
|
||||
import { validate as validateJsonSchema } from 'jsonschema';
|
||||
import path from 'path';
|
||||
import { PassThrough } from 'stream';
|
||||
import * as winston from 'winston';
|
||||
import { Logger } from 'winston';
|
||||
import { parseRepoUrl } from '../actions/builtin/publish/util';
|
||||
import { TemplateActionRegistry } from '../actions/TemplateActionRegistry';
|
||||
import { isTruthy } from './helper';
|
||||
import { Task, TaskBroker } from './types';
|
||||
|
||||
type Options = {
|
||||
logger: Logger;
|
||||
@@ -48,6 +49,8 @@ export class TaskWorker {
|
||||
});
|
||||
|
||||
this.handlebars.registerHelper('json', obj => JSON.stringify(obj));
|
||||
|
||||
this.handlebars.registerHelper('not', value => !isTruthy(value));
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -102,6 +105,47 @@ export class TaskWorker {
|
||||
});
|
||||
|
||||
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,
|
||||
strict: true,
|
||||
data: false,
|
||||
preventIndent: true,
|
||||
})(templateCtx);
|
||||
|
||||
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',
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { isTruthy } from './helper';
|
||||
|
||||
describe('isTruthy', () => {
|
||||
it.each`
|
||||
value | result
|
||||
${'string'} | ${true}
|
||||
${true} | ${true}
|
||||
${1} | ${true}
|
||||
${['1']} | ${true}
|
||||
${{}} | ${true}
|
||||
${false} | ${false}
|
||||
${''} | ${false}
|
||||
${undefined} | ${false}
|
||||
${null} | ${false}
|
||||
${0} | ${false}
|
||||
${[]} | ${false}
|
||||
`('should be $result for $value', async ({ value, result }) => {
|
||||
expect(isTruthy(value)).toEqual(result);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { isArray } from 'lodash';
|
||||
|
||||
/**
|
||||
* Returns true if the input is not `false`, `undefined`, `null`, `""`, `0`, or
|
||||
* `[]`. This behavior is based on the behavior of handlebars, see
|
||||
* https://handlebarsjs.com/guide/builtin-helpers.html#if
|
||||
*/
|
||||
export function isTruthy(value: any): boolean {
|
||||
return isArray(value) ? value.length > 0 : !!value;
|
||||
}
|
||||
@@ -51,6 +51,7 @@ export type TaskSpec = {
|
||||
name: string;
|
||||
action: string;
|
||||
input?: JsonObject;
|
||||
if?: string;
|
||||
}>;
|
||||
output: { [name: string]: string };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user