Merge pull request #13704 from RoadieHQ/scaffolder/configure-globals-on-nunjucks
make nunjucks' globals configureable
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user