Add a contrib document to show how to test scaffolder templates locally
Signed-off-by: Axel Hecht <axel@pike.org>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Scaffolder contrib tools
|
||||
|
||||
## Testing Templates with Dry-run
|
||||
|
||||
Scaffolder templates support anything that backstage.io and custom actions can do, so testing them is hard without actually running the instance of Backstage that they're designed for.
|
||||
|
||||
The [commandline script](template-testing-dry-run.md) might offer a way for you to do so using the dry-run API used for the Template Editor. Run it against a running instance, either locally or remote, and use it like
|
||||
|
||||
```sh
|
||||
scaffolder-dry http://localhost:7007/ template-directory values.yml output-directory
|
||||
```
|
||||
|
||||
If you're using backend-to-backend authentication, either
|
||||
|
||||
- pass a front-end auth token from a current browser session via `--token $FRONTEND_TOKEN`,
|
||||
- have the tool create a b2b token for a given base64 encoded backend secret via `--backend-secret $BACKEND_SECRET`.
|
||||
@@ -0,0 +1,168 @@
|
||||
```js
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A CLI that helps you test Backstage software templates
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import { ensureDir } from 'fs-extra';
|
||||
import { program } from 'commander';
|
||||
import { base64url, exportJWK, generateSecret, jwtVerify, SignJWT } from 'jose';
|
||||
import fetch from 'node-fetch';
|
||||
import readdir from 'recursive-readdir';
|
||||
import { parse } from 'yaml';
|
||||
import { version } from '../../../package.json';
|
||||
import type { ScaffolderDryRunResponse } from '@backstage/plugin-scaffolder';
|
||||
|
||||
const TOKEN_ALG = 'HS256';
|
||||
const TOKEN_SUB = 'backstage-server';
|
||||
|
||||
const loadDirectoryContents = async (template_path: string) => {
|
||||
const files = await await readdir(template_path, ['.git']);
|
||||
const contents = await Promise.all(
|
||||
files.map(async p => {
|
||||
return {
|
||||
path: relative(template_path, p),
|
||||
base64Content: (await readFile(p)).toString('base64'),
|
||||
};
|
||||
}),
|
||||
);
|
||||
return contents;
|
||||
};
|
||||
|
||||
const writeResultContents = async (
|
||||
root: string,
|
||||
directoryContents: ScaffolderDryRunResponse['directoryContents'],
|
||||
) => {
|
||||
const directories = new Set(
|
||||
directoryContents.map(({ path }) => dirname(join(root, path))),
|
||||
);
|
||||
await Promise.all(Array.from(directories).map(d => ensureDir(d)));
|
||||
await Promise.all(
|
||||
directoryContents.map(async ({ path, base64Content, executable }) =>
|
||||
writeFile(join(root, path), Buffer.from(base64Content, 'base64'), {
|
||||
mode: executable ? 0o755 : 0o644,
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const getToken = async (backendSecret: string) => {
|
||||
const signingKey = base64url.decode(backendSecret);
|
||||
return await new SignJWT({})
|
||||
.setProtectedHeader({ alg: TOKEN_ALG })
|
||||
.setSubject(TOKEN_SUB)
|
||||
.setExpirationTime('10min')
|
||||
.sign(signingKey);
|
||||
};
|
||||
|
||||
const api = async (
|
||||
bodyObj: Record<string, any>,
|
||||
{ baseURL, token }: { baseURL: string; token: string | false },
|
||||
) => {
|
||||
const body = gzipSync(JSON.stringify(bodyObj));
|
||||
return fetch(new URL('/api/scaffolder/v2/dry-run', baseURL), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token || ''}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Encoding': 'gzip',
|
||||
},
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
const handle = async (
|
||||
baseURL: string,
|
||||
template_path: string,
|
||||
data: string,
|
||||
target: string,
|
||||
{
|
||||
token,
|
||||
backendSecret,
|
||||
}: { token: string | false; backendSecret: string | false },
|
||||
) => {
|
||||
const directoryContents = await loadDirectoryContents(template_path);
|
||||
const values = parse(await readFile(data, 'utf-8'));
|
||||
const secrets = {};
|
||||
const template = parse(
|
||||
await readFile(`${template_path}/template.yaml`, 'utf-8'),
|
||||
);
|
||||
if (backendSecret) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
token = await getToken(backendSecret);
|
||||
}
|
||||
const response = await api(
|
||||
{
|
||||
directoryContents,
|
||||
values,
|
||||
secrets,
|
||||
template,
|
||||
},
|
||||
{ baseURL, token },
|
||||
);
|
||||
if (!response.ok) {
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType?.startsWith('application/json')) {
|
||||
const responseData = await response.json();
|
||||
if (responseData.error) {
|
||||
throw responseData.error;
|
||||
}
|
||||
if (responseData.errors) {
|
||||
throw responseData.errors;
|
||||
}
|
||||
throw responseData;
|
||||
}
|
||||
throw await response.text();
|
||||
}
|
||||
const { log, directoryContents: resultContents } =
|
||||
(await response.json()) as ScaffolderDryRunResponse;
|
||||
for (const logEntry of log) {
|
||||
console.log(logEntry.body.message);
|
||||
}
|
||||
await writeResultContents(target, resultContents);
|
||||
};
|
||||
|
||||
const main = async (argv: string[]) => {
|
||||
program
|
||||
.name('backstage-scaffolder')
|
||||
.version(version)
|
||||
.description('Creates a dry-run output of a given template')
|
||||
.option('-t, --token <value>', 'JWT to use for auth')
|
||||
.option('-b, --backend-secret <value>', 'Base64 encoded backend secret')
|
||||
.argument('url', 'URL of your Backstage instance')
|
||||
.argument('template-path', 'Source directory of template')
|
||||
.argument('data-path', 'YAML file with input to render')
|
||||
.argument('target', 'Output directory')
|
||||
.action(handle);
|
||||
|
||||
await program.parseAsync(argv);
|
||||
process.exit();
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', rejection => {
|
||||
console.error(rejection);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
main(process.argv);
|
||||
```
|
||||
Reference in New Issue
Block a user