-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Add to the module of Quarkus a template filter. #114 Signed-off-by: cmoulliard <cmoulliard@redhat.com> * Move the utility functions to the util folder Signed-off-by: cmoulliard <cmoulliard@redhat.com> * Fix warning messages reported by yarn test Signed-off-by: cmoulliard <cmoulliard@redhat.com> * Remove import non needed Signed-off-by: cmoulliard <cmoulliard@redhat.com> --------- Signed-off-by: cmoulliard <cmoulliard@redhat.com>
- Loading branch information
1 parent
229ad62
commit 725c168
Showing
9 changed files
with
324 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,12 @@ | ||
/** | ||
* A module for the scaffolder backend that lets you interact with code.quarkus.io to create application | ||
* A module for the scaffolder backend | ||
* | ||
* It provides scaffolder actions like template filters | ||
* | ||
* @packageDocumentation | ||
*/ | ||
export * from './scaffolder/actions/quarkus'; | ||
export * from './scaffolder/filters/version'; | ||
export { quarkusModule as default } from './module'; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
212 changes: 212 additions & 0 deletions
212
plugins/quarkus-backend/src/scaffolder/filters/util/SecureTemplater.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
// TODO: Investigate if we could import ir from the plugin-scaffolder-backend package vs copy/paste the code here ! | ||
|
||
import { Isolate } from 'isolated-vm'; | ||
import { resolvePackagePath } from '@backstage/backend-plugin-api'; | ||
import { | ||
TemplateFilter as _TemplateFilter, | ||
TemplateGlobal as _TemplateGlobal, | ||
} from '@backstage/plugin-scaffolder-node'; | ||
import fs from 'fs-extra'; | ||
import { JsonValue } from '@backstage/types'; | ||
import { getMajorNodeVersion, isNoNodeSnapshotOptionProvided } from './helpers'; | ||
|
||
// language=JavaScript | ||
const mkScript = (nunjucksSource: string) => ` | ||
const { render, renderCompat } = (() => { | ||
const module = {}; | ||
const process = { env: {} }; | ||
const require = (pkg) => { if (pkg === 'events') { return function (){}; }}; | ||
${nunjucksSource} | ||
const env = module.exports.configure({ | ||
autoescape: false, | ||
...JSON.parse(nunjucksConfigs), | ||
tags: { | ||
variableStart: '\${{', | ||
variableEnd: '}}', | ||
}, | ||
}); | ||
const compatEnv = module.exports.configure({ | ||
autoescape: false, | ||
...JSON.parse(nunjucksConfigs), | ||
tags: { | ||
variableStart: '{{', | ||
variableEnd: '}}', | ||
}, | ||
}); | ||
compatEnv.addFilter('jsonify', compatEnv.getFilter('dump')); | ||
for (const name of JSON.parse(availableTemplateFilters)) { | ||
env.addFilter(name, (...args) => JSON.parse(callFilter(name, args))); | ||
} | ||
for (const [name, value] of Object.entries(JSON.parse(availableTemplateGlobals))) { | ||
env.addGlobal(name, value); | ||
} | ||
for (const name of JSON.parse(availableTemplateCallbacks)) { | ||
env.addGlobal(name, (...args) => JSON.parse(callGlobal(name, args))); | ||
} | ||
let uninstallCompat = undefined; | ||
function render(str, values) { | ||
try { | ||
if (uninstallCompat) { | ||
uninstallCompat(); | ||
uninstallCompat = undefined; | ||
} | ||
return env.renderString(str, JSON.parse(values)); | ||
} catch (error) { | ||
// Make sure errors don't leak anything | ||
throw new Error(String(error.message)); | ||
} | ||
} | ||
function renderCompat(str, values) { | ||
try { | ||
if (!uninstallCompat) { | ||
uninstallCompat = module.exports.installJinjaCompat(); | ||
} | ||
return compatEnv.renderString(str, JSON.parse(values)); | ||
} catch (error) { | ||
// Make sure errors don't leak anything | ||
throw new Error(String(error.message)); | ||
} | ||
} | ||
return { render, renderCompat }; | ||
})(); | ||
`; | ||
|
||
/** | ||
* @public | ||
* @deprecated Import from `@backstage/plugin-scaffolder-node` instead. | ||
*/ | ||
export type TemplateFilter = _TemplateFilter; | ||
|
||
/** | ||
* @public | ||
* @deprecated Import from `@backstage/plugin-scaffolder-node` instead. | ||
*/ | ||
export type TemplateGlobal = _TemplateGlobal; | ||
|
||
interface SecureTemplaterOptions { | ||
/* Enables jinja compatibility and the "jsonify" filter */ | ||
cookiecutterCompat?: boolean; | ||
/* Extra user-provided nunjucks filters */ | ||
templateFilters?: Record<string, TemplateFilter>; | ||
/* Extra user-provided nunjucks globals */ | ||
templateGlobals?: Record<string, TemplateGlobal>; | ||
nunjucksConfigs?: { trimBlocks?: boolean; lstripBlocks?: boolean }; | ||
} | ||
|
||
export type SecureTemplateRenderer = ( | ||
template: string, | ||
values: unknown, | ||
) => string; | ||
|
||
export class SecureTemplater { | ||
static async loadRenderer(options: SecureTemplaterOptions = {}) { | ||
const { | ||
cookiecutterCompat, | ||
templateFilters = {}, | ||
templateGlobals = {}, | ||
nunjucksConfigs = {}, | ||
} = options; | ||
|
||
const nodeVersion = getMajorNodeVersion(); | ||
if (nodeVersion >= 20 && !isNoNodeSnapshotOptionProvided()) { | ||
throw new Error( | ||
`When using Node.js version 20 or newer, the scaffolder backend plugin requires that it be started with the --no-node-snapshot option. | ||
Please make sure that you have NODE_OPTIONS=--no-node-snapshot in your environment.`, | ||
); | ||
} | ||
|
||
const isolate = new Isolate({ memoryLimit: 128 }); | ||
const context = await isolate.createContext(); | ||
const contextGlobal = context.global; | ||
|
||
const nunjucksSource = await fs.readFile( | ||
resolvePackagePath( | ||
'@backstage/plugin-scaffolder-backend', | ||
'assets/nunjucks.js.txt', | ||
), | ||
'utf-8', | ||
); | ||
|
||
const nunjucksScript = await isolate.compileScript( | ||
mkScript(nunjucksSource), | ||
); | ||
|
||
await contextGlobal.set('nunjucksConfigs', JSON.stringify(nunjucksConfigs)); | ||
|
||
const availableFilters = Object.keys(templateFilters); | ||
|
||
await contextGlobal.set( | ||
'availableTemplateFilters', | ||
JSON.stringify(availableFilters), | ||
); | ||
|
||
const globalCallbacks = []; | ||
const globalValues: Record<string, unknown> = {}; | ||
for (const [name, value] of Object.entries(templateGlobals)) { | ||
if (typeof value === 'function') { | ||
globalCallbacks.push(name); | ||
} else { | ||
globalValues[name] = value; | ||
} | ||
} | ||
|
||
await contextGlobal.set( | ||
'availableTemplateGlobals', | ||
JSON.stringify(globalValues), | ||
); | ||
await contextGlobal.set( | ||
'availableTemplateCallbacks', | ||
JSON.stringify(globalCallbacks), | ||
); | ||
|
||
await contextGlobal.set( | ||
'callFilter', | ||
(filterName: string, args: JsonValue[]) => { | ||
if (!Object.hasOwn(templateFilters, filterName)) { | ||
return ''; | ||
} | ||
return JSON.stringify(templateFilters[filterName](...args)); | ||
}, | ||
); | ||
|
||
await contextGlobal.set( | ||
'callGlobal', | ||
(globalName: string, args: JsonValue[]) => { | ||
if (!Object.hasOwn(templateGlobals, globalName)) { | ||
return ''; | ||
} | ||
const global = templateGlobals[globalName]; | ||
if (typeof global !== 'function') { | ||
return ''; | ||
} | ||
return JSON.stringify(global(...args)); | ||
}, | ||
); | ||
|
||
await nunjucksScript.run(context); | ||
|
||
const render: SecureTemplateRenderer = (template, values) => { | ||
if (!context) { | ||
throw new Error('SecureTemplater has not been initialized'); | ||
} | ||
|
||
contextGlobal.setSync('templateStr', String(template)); | ||
contextGlobal.setSync('templateValues', JSON.stringify(values)); | ||
|
||
if (cookiecutterCompat) { | ||
return context.evalSync(`renderCompat(templateStr, templateValues)`); | ||
} | ||
|
||
return context.evalSync(`render(templateStr, templateValues)`); | ||
}; | ||
return render; | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
plugins/quarkus-backend/src/scaffolder/filters/util/helpers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* | ||
* Copyright 2024 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 function isNoNodeSnapshotOptionProvided(): boolean { | ||
return ( | ||
process.env.NODE_OPTIONS?.includes('--no-node-snapshot') || | ||
process.argv.includes('--no-node-snapshot') | ||
); | ||
} | ||
|
||
/** | ||
* Gets the major version of the currently running Node.js process. | ||
* | ||
* @remarks | ||
* This function extracts the major version from `process.versions.node` (a string representing the Node.js version), | ||
* which includes the major, minor, and patch versions. It splits this string by the `.` character to get an array | ||
* of these versions, and then parses the first element of this array (the major version) to a number. | ||
* | ||
* @returns {number} The major version of the currently running Node.js process. | ||
*/ | ||
export function getMajorNodeVersion(): number { | ||
const version = process.versions.node; | ||
return parseInt(version.split('.')[0], 10); | ||
} |
1 change: 1 addition & 0 deletions
1
plugins/quarkus-backend/src/scaffolder/filters/version/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { extractVersionFromKey } from './version'; |
28 changes: 28 additions & 0 deletions
28
plugins/quarkus-backend/src/scaffolder/filters/version/version.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { SecureTemplater } from '../util/SecureTemplater'; | ||
import {extractVersionFromKey} from "./version"; | ||
|
||
describe('QuarkusStreamKey', () => { | ||
it('should get an error as streamKey is not well formatted => io.quarkus.platform_3.9 ', async () => { | ||
const renderWith = await SecureTemplater.loadRenderer({ | ||
templateFilters: { | ||
extractVersionFromKey: (streamKey) => extractVersionFromKey(streamKey as string), | ||
}, | ||
}); | ||
|
||
let ctx = {inputValue: 'io.quarkus.platform_3.9'}; | ||
expect(() => renderWith('${{ inputValue | extractVersionFromKey }}', ctx),).toThrow( | ||
/Error: The streamKey is not formatted as: io.quarkus.platform:<version>/, | ||
); | ||
}); | ||
|
||
it('should not get an error as streamKey is well formatted => io.quarkus.platform:3.9 ', async () => { | ||
const renderWith = await SecureTemplater.loadRenderer({ | ||
templateFilters: { | ||
extractVersionFromKey: (streamKey) => extractVersionFromKey(streamKey as string), | ||
}, | ||
}); | ||
|
||
let ctx = {inputValue: 'io.quarkus.platform:3.10'}; | ||
expect(renderWith('${{ inputValue | extractVersionFromKey }}', ctx)).toBe('3.10'); | ||
}); | ||
}); |
14 changes: 14 additions & 0 deletions
14
plugins/quarkus-backend/src/scaffolder/filters/version/version.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
export function extractVersionFromKey( | ||
streamKey: string, | ||
): string { | ||
if (!streamKey) { | ||
throw new Error(`StreamKey to be processed cannot be empty`); | ||
} | ||
|
||
let streamKeyArr = streamKey.split(":") | ||
if (streamKeyArr.length < 2) { | ||
throw new Error(`The streamKey is not formatted as: io.quarkus.platform:\<version\>`); | ||
} else { | ||
return streamKeyArr[1] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters