scaffolder-backend: add dry-run implementation

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-04-04 10:14:41 +02:00
parent 22318e7c98
commit f45a2abcf2
3 changed files with 199 additions and 0 deletions
@@ -0,0 +1,39 @@
/*
* 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 { JsonObject } from '@backstage/types';
import { TemplateAction, TemplateActionRegistry } from '../actions';
/** @internal */
export class DecoratedActionsRegistry extends TemplateActionRegistry {
constructor(
private readonly innerRegistry: TemplateActionRegistry,
extraActions: Array<TemplateAction<JsonObject>>,
) {
super();
for (const action of extraActions) {
this.register(action);
}
}
get(actionId: string): TemplateAction<JsonObject> {
try {
return super.get(actionId);
} catch {
return this.innerRegistry.get(actionId);
}
}
}
@@ -0,0 +1,143 @@
/*
* 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 { ScmIntegrations } from '@backstage/integration';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { JsonObject } from '@backstage/types';
import { v4 as uuid } from 'uuid';
import { pathToFileURL } from 'url';
import { Logger } from 'winston';
import {
deserializeDirectoryContents,
SerializedFile,
serializeDirectoryContents,
} from '../../lib/files';
import { TemplateFilter } from '../../lib/templating';
import { createTemplateAction, TemplateActionRegistry } from '../actions';
import { NunjucksWorkflowRunner } from '../tasks/NunjucksWorkflowRunner';
import { TaskSecrets } from '../tasks/types';
import { DecoratedActionsRegistry } from './DecoratedActionsRegistry';
import fs from 'fs-extra';
import { resolveSafeChildPath } from '@backstage/backend-common';
interface DryRunInput {
spec: TaskSpec;
secrets?: TaskSecrets;
content: SerializedFile[];
}
interface DryRunResult {
log: JsonObject[];
content: SerializedFile[];
output: JsonObject;
}
/** @internal */
export type TemplateTesterCreateOptions = {
logger: Logger;
integrations: ScmIntegrations;
actionRegistry: TemplateActionRegistry;
workingDirectory: string;
additionalTemplateFilters?: Record<string, TemplateFilter>;
};
/**
* Executes a dry-run of the provided template.
*
* The provided content will be extracted into a temporary directory
* which is then use as the base for any relative file fetch paths.
*
* @internal
*/
export function createDryRunner(options: TemplateTesterCreateOptions) {
return async function dryRun(input: DryRunInput): Promise<DryRunResult> {
let contentPromise;
const workflowRunner = new NunjucksWorkflowRunner({
...options,
actionRegistry: new DecoratedActionsRegistry(options.actionRegistry, [
createTemplateAction({
id: 'dry-run:extract',
supportsDryRun: true,
async handler(ctx) {
contentPromise = serializeDirectoryContents(ctx.workspacePath);
await contentPromise.catch(() => {});
},
}),
]),
});
const dryRunId = uuid();
const log = new Array<JsonObject>();
const contentsPath = resolveSafeChildPath(
options.workingDirectory,
`dry-run-content-${dryRunId}`,
);
try {
await deserializeDirectoryContents(contentsPath, input.content);
const result = await workflowRunner.execute({
spec: {
...input.spec,
steps: [
...input.spec.steps,
{
id: dryRunId,
name: 'dry-run:extract',
action: 'dry-run:extract',
},
],
templateInfo: {
entityRef: 'template:default/dry-run',
baseUrl: pathToFileURL(
resolveSafeChildPath(contentsPath, 'template.yaml'),
).toString(),
},
},
secrets: input.secrets,
done: false,
isDryRun: true,
getWorkspaceName: async () => `dry-run-${dryRunId}`,
async emitLog(message: string, logMetadata?: JsonObject) {
if (logMetadata?.stepId === dryRunId) {
return;
}
log.push({
...logMetadata,
message,
});
},
async complete() {
throw new Error('Not implemented');
},
});
if (!contentPromise) {
throw new Error('Content extraction step was skipped');
}
const content = await contentPromise;
return {
log,
content,
output: result.output,
};
} finally {
await fs.remove(contentsPath);
}
};
}
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { createDryRunner } from './createDryRunner';