Merge pull request #13704 from RoadieHQ/scaffolder/configure-globals-on-nunjucks

make nunjucks' globals configureable
This commit is contained in:
Patrik Oldsberg
2022-09-27 12:49:32 +02:00
committed by GitHub
12 changed files with 118 additions and 11 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
Added a new property called `additionalTemplateGlobals` which allows you to add global functions to the scaffolder nunjucks templates.
+12
View File
@@ -60,6 +60,8 @@ export const createBuiltinActions: (
// @public
export interface CreateBuiltInActionsOptions {
additionalTemplateFilters?: Record<string, TemplateFilter>;
// (undocumented)
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
catalogClient: CatalogApi;
config: Config;
integrations: ScmIntegrations;
@@ -108,6 +110,7 @@ export function createFetchTemplateAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
}): TemplateAction<{
url: string;
targetPath?: string | undefined;
@@ -450,6 +453,7 @@ export type CreateWorkerOptions = {
workingDirectory: string;
logger: Logger;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
};
// @public
@@ -540,6 +544,8 @@ export interface RouterOptions {
// (undocumented)
additionalTemplateFilters?: Record<string, TemplateFilter>;
// (undocumented)
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
// (undocumented)
catalogClient: CatalogApi;
// (undocumented)
config: Config;
@@ -595,6 +601,7 @@ export type ScaffolderPluginOptions = {
taskWorkers?: number;
taskBroker?: TaskBroker;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
};
// @public
@@ -815,4 +822,9 @@ export class TemplateActionRegistry {
// @public (undocumented)
export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined;
// @public (undocumented)
export type TemplateGlobal =
| ((...args: JsonValue[]) => JsonValue | undefined)
| JsonValue;
```
@@ -26,7 +26,7 @@ import {
} from '@backstage/backend-plugin-api';
import { ScmIntegrations } from '@backstage/integration';
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
import { TemplateFilter } from './lib';
import { TemplateFilter, TemplateGlobal } from './lib';
import { createBuiltinActions, TaskBroker, TemplateAction } from './scaffolder';
import { createRouter } from './service/router';
@@ -39,6 +39,7 @@ export type ScaffolderPluginOptions = {
taskWorkers?: number;
taskBroker?: TaskBroker;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
};
/**
@@ -101,7 +102,12 @@ export const scaffolderPlugin = createBackendPlugin({
httpRouter,
catalogClient,
}) {
const { additionalTemplateFilters, taskBroker, taskWorkers } = options;
const {
additionalTemplateFilters,
taskBroker,
taskWorkers,
additionalTemplateGlobals,
} = options;
const log = loggerToWinstonLogger(logger);
const actions = options.actions || [
@@ -112,6 +118,7 @@ export const scaffolderPlugin = createBackendPlugin({
reader,
config,
additionalTemplateFilters,
additionalTemplateGlobals,
}),
];
@@ -130,6 +137,7 @@ export const scaffolderPlugin = createBackendPlugin({
taskBroker,
taskWorkers,
additionalTemplateFilters,
additionalTemplateGlobals,
});
httpRouter.use(router);
},
@@ -144,6 +144,27 @@ describe('SecureTemplater', () => {
['the input value', 'another extra arg'],
]);
});
it('should make additional globals available when requested', async () => {
const mockGlobal1 = jest.fn(() => 'awesome global function');
const mockGlobal2 = 'foo';
const mockGlobal3 = 123456;
const renderWith = await SecureTemplater.loadRenderer({
additionalTemplateGlobals: { mockGlobal1, mockGlobal2, mockGlobal3 },
});
const renderWithout = await SecureTemplater.loadRenderer();
const ctx = {};
expect(renderWith('${{ mockGlobal1() }}', ctx)).toBe(
'awesome global function',
);
expect(renderWith('${{ mockGlobal2 }}', ctx)).toBe('foo');
expect(renderWith('${{ mockGlobal3 }}', ctx)).toBe('123456');
expect(() => renderWithout('${{ mockGlobal1() }}', ctx)).toThrow(
/Error: Unable to call `mockGlobal1`/,
);
});
it('should not allow helpers to be rewritten', async () => {
const render = await SecureTemplater.loadRenderer({
@@ -64,6 +64,16 @@ const { render, renderCompat } = (() => {
}
}
if (typeof additionalTemplateGlobals !== 'undefined') {
for (const [globalName, global] of Object.entries(additionalTemplateGlobals)) {
if (typeof global === 'function') {
env.addGlobal(globalName, (...args) => JSON.parse(global(...args)));
} else {
env.addGlobal(globalName, JSON.parse(global));
}
}
}
let uninstallCompat = undefined;
function render(str, values) {
@@ -98,6 +108,11 @@ const { render, renderCompat } = (() => {
/** @public */
export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined;
/** @public */
export type TemplateGlobal =
| ((...args: JsonValue[]) => JsonValue | undefined)
| JsonValue;
export interface SecureTemplaterOptions {
/* Optional implementation of the parseRepoUrl filter */
parseRepoUrl?(repoUrl: string): RepoSpec;
@@ -107,6 +122,8 @@ export interface SecureTemplaterOptions {
/* Extra user-provided nunjucks filters */
additionalTemplateFilters?: Record<string, TemplateFilter>;
/* Extra user-provided nunjucks globals */
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
}
export type SecureTemplateRenderer = (
@@ -116,8 +133,12 @@ export type SecureTemplateRenderer = (
export class SecureTemplater {
static async loadRenderer(options: SecureTemplaterOptions = {}) {
const { parseRepoUrl, cookiecutterCompat, additionalTemplateFilters } =
options;
const {
parseRepoUrl,
cookiecutterCompat,
additionalTemplateFilters,
additionalTemplateGlobals,
} = options;
const sandbox: Record<string, any> = {};
if (parseRepoUrl) {
@@ -134,7 +155,21 @@ export class SecureTemplater {
]),
);
}
if (additionalTemplateGlobals) {
sandbox.additionalTemplateGlobals = Object.fromEntries(
Object.entries(additionalTemplateGlobals)
.filter(([_, global]) => !!global)
.map(([globalName, global]) => {
if (typeof global === 'function') {
return [
globalName,
(...args: JsonValue[]) => JSON.stringify(global(...args)),
];
}
return [globalName, JSON.stringify(global)];
}),
);
}
const vm = new VM({ sandbox });
const nunjucksSource = await fs.readFile(
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export type { TemplateFilter } from './SecureTemplater';
export type { TemplateFilter, TemplateGlobal } from './SecureTemplater';
@@ -28,7 +28,7 @@ import {
createCatalogWriteAction,
} from './catalog';
import { TemplateFilter } from '../../../lib';
import { TemplateFilter, TemplateGlobal } from '../../../lib';
import { TemplateAction } from '../types';
import { createDebugLogAction } from './debug';
import { createFetchPlainAction, createFetchTemplateAction } from './fetch';
@@ -82,6 +82,7 @@ export interface CreateBuiltInActionsOptions {
* Template Manifests and also template skeleton files when using `fetch:template`.
*/
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
}
/**
@@ -100,6 +101,7 @@ export const createBuiltinActions = (
catalogClient,
config,
additionalTemplateFilters,
additionalTemplateGlobals,
} = options;
const githubCredentialsProvider: GithubCredentialsProvider =
@@ -114,6 +116,7 @@ export const createBuiltinActions = (
integrations,
reader,
additionalTemplateFilters,
additionalTemplateGlobals,
}),
createPublishGerritAction({
integrations,
@@ -26,6 +26,7 @@ import { isBinaryFile } from 'isbinaryfile';
import {
TemplateFilter,
SecureTemplater,
TemplateGlobal,
} from '../../../../lib/templating/SecureTemplater';
/**
@@ -39,8 +40,14 @@ export function createFetchTemplateAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
}) {
const { reader, integrations, additionalTemplateFilters } = options;
const {
reader,
integrations,
additionalTemplateFilters,
additionalTemplateGlobals,
} = options;
return createTemplateAction<{
url: string;
@@ -218,6 +225,7 @@ export function createFetchTemplateAction(options: {
const renderTemplate = await SecureTemplater.loadRenderer({
cookiecutterCompat: ctx.input.cookiecutterCompat,
additionalTemplateFilters,
additionalTemplateGlobals,
});
for (const location of allEntriesInTemplate) {
@@ -25,7 +25,7 @@ import {
SerializedFile,
serializeDirectoryContents,
} from '../../lib/files';
import { TemplateFilter } from '../../lib/templating';
import { TemplateFilter, TemplateGlobal } from '../../lib/templating';
import { createTemplateAction, TemplateActionRegistry } from '../actions';
import { NunjucksWorkflowRunner } from '../tasks/NunjucksWorkflowRunner';
import { TaskSecrets } from '../tasks/types';
@@ -52,6 +52,7 @@ export type TemplateTesterCreateOptions = {
actionRegistry: TemplateActionRegistry;
workingDirectory: string;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
};
/**
@@ -31,6 +31,7 @@ import {
TemplateFilter,
SecureTemplater,
SecureTemplateRenderer,
TemplateGlobal,
} from '../../lib/templating/SecureTemplater';
import {
TaskSpec,
@@ -45,6 +46,7 @@ type NunjucksWorkflowRunnerOptions = {
integrations: ScmIntegrations;
logger: winston.Logger;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
};
type TemplateContext = {
@@ -197,6 +199,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
return parseRepoUrl(url, integrations);
},
additionalTemplateFilters: this.options.additionalTemplateFilters,
additionalTemplateGlobals: this.options.additionalTemplateGlobals,
});
try {
@@ -20,7 +20,10 @@ import { Logger } from 'winston';
import { TemplateActionRegistry } from '../actions';
import { ScmIntegrations } from '@backstage/integration';
import { assertError } from '@backstage/errors';
import { TemplateFilter } from '../../lib/templating/SecureTemplater';
import {
TemplateFilter,
TemplateGlobal,
} from '../../lib/templating/SecureTemplater';
/**
* TaskWorkerOptions
@@ -46,6 +49,7 @@ export type CreateWorkerOptions = {
workingDirectory: string;
logger: Logger;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
};
/**
@@ -64,6 +68,7 @@ export class TaskWorker {
integrations,
workingDirectory,
additionalTemplateFilters,
additionalTemplateGlobals,
} = options;
const workflowRunner = new NunjucksWorkflowRunner({
@@ -72,6 +77,7 @@ export class TaskWorker {
logger,
workingDirectory,
additionalTemplateFilters,
additionalTemplateGlobals,
});
return new TaskWorker({
@@ -36,7 +36,7 @@ import Router from 'express-promise-router';
import { validate } from 'jsonschema';
import { Logger } from 'winston';
import { z } from 'zod';
import { TemplateFilter } from '../lib';
import { TemplateFilter, TemplateGlobal } from '../lib';
import {
createBuiltinActions,
DatabaseTaskStore,
@@ -70,6 +70,7 @@ export interface RouterOptions {
taskWorkers?: number;
taskBroker?: TaskBroker;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
identity?: IdentityApi;
}
@@ -161,6 +162,7 @@ export async function createRouter(
taskWorkers,
scheduler,
additionalTemplateFilters,
additionalTemplateGlobals,
} = options;
const logger = parentLogger.child({ plugin: 'scaffolder' });
@@ -208,6 +210,7 @@ export async function createRouter(
logger,
workingDirectory,
additionalTemplateFilters,
additionalTemplateGlobals,
});
workers.push(worker);
}
@@ -220,6 +223,7 @@ export async function createRouter(
reader,
config,
additionalTemplateFilters,
additionalTemplateGlobals,
});
actionsToRegister.forEach(action => actionRegistry.register(action));
@@ -231,6 +235,7 @@ export async function createRouter(
logger,
workingDirectory,
additionalTemplateFilters,
additionalTemplateGlobals,
});
router