feat(scaffolder): error handling, log propagation
Co-authored-by: Ben Lambert <ben@blam.sh>
This commit is contained in:
@@ -16,8 +16,11 @@
|
||||
import { CookieCutter } from './cookiecutter';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import Stream, { PassThrough } from 'stream';
|
||||
|
||||
const mockDocker = { run: jest.fn() };
|
||||
const mockDocker = {
|
||||
run: jest.fn<any, any>(() => [{ Error: null, StatusCode: 0 }]),
|
||||
};
|
||||
jest.mock(
|
||||
'dockerode',
|
||||
() =>
|
||||
@@ -32,7 +35,7 @@ describe('CookieCutter Templater', () => {
|
||||
const cookie = new CookieCutter();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should write a cookiecutter.json file with the values from the entitiy', async () => {
|
||||
@@ -85,8 +88,8 @@ describe('CookieCutter Templater', () => {
|
||||
// to @backstage/cookiecutter in docker hub.
|
||||
expect(mockDocker.run).toHaveBeenCalledWith(
|
||||
'backstage/cookiecutter',
|
||||
['cookiecutter', '--no-input', '-o', '/result', '/template', '--verbose'],
|
||||
process.stdout,
|
||||
['cookiecutter', '--no-input', '-o', '/result', '/template'],
|
||||
expect.any(Stream),
|
||||
expect.objectContaining({
|
||||
HostConfig: {
|
||||
Binds: expect.arrayContaining([
|
||||
@@ -116,4 +119,63 @@ describe('CookieCutter Templater', () => {
|
||||
|
||||
expect(path).toBe(`${realpath}/result`);
|
||||
});
|
||||
|
||||
it('throws a correct error if the templating fails in docker', async () => {
|
||||
mockDocker.run.mockResolvedValueOnce([
|
||||
{
|
||||
Error: new Error('Something went wrong with docker'),
|
||||
StatusCode: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const tempdir = os.tmpdir();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
};
|
||||
|
||||
await expect(cookie.run({ directory: tempdir, values })).rejects.toThrow(
|
||||
/Something went wrong with docker/,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the passed stream as a log stream', async () => {
|
||||
const logStream = new PassThrough();
|
||||
const tempdir = os.tmpdir();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
};
|
||||
|
||||
await cookie.run({ directory: tempdir, values, logStream });
|
||||
|
||||
expect(mockDocker.run).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(Array),
|
||||
logStream,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a correct error if the container returns a non-zero exit code', async () => {
|
||||
mockDocker.run.mockResolvedValueOnce([
|
||||
{
|
||||
Error: null,
|
||||
StatusCode: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const tempdir = os.tmpdir();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
};
|
||||
|
||||
await expect(cookie.run({ directory: tempdir, values })).rejects.toThrow(
|
||||
/Docker container returned a non-zero exit code \(1\)/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import { TemplaterBase, TemplaterRunOptions } from '.';
|
||||
import fs from 'fs-extra';
|
||||
import Docker from 'dockerode';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
|
||||
import { PassThrough } from 'stream';
|
||||
export class CookieCutter implements TemplaterBase {
|
||||
private docker: Docker;
|
||||
constructor() {
|
||||
@@ -51,10 +51,12 @@ export class CookieCutter implements TemplaterBase {
|
||||
const realTemplatePath = await fs.promises.realpath(options.directory);
|
||||
const outDir = `${realTemplatePath}/result`;
|
||||
|
||||
await this.docker.run(
|
||||
const [
|
||||
{ Error: dockerError, StatusCode: containerStatusCode },
|
||||
] = await this.docker.run(
|
||||
'backstage/cookiecutter',
|
||||
['cookiecutter', '--no-input', '-o', '/result', '/template', '--verbose'],
|
||||
process.stdout,
|
||||
['cookiecutter', '--no-input', '-o', '/result', '/template'],
|
||||
options.logStream ?? new PassThrough(),
|
||||
{
|
||||
Volumes: { '/result': {}, '/template': {} },
|
||||
HostConfig: {
|
||||
@@ -63,6 +65,18 @@ export class CookieCutter implements TemplaterBase {
|
||||
},
|
||||
);
|
||||
|
||||
if (dockerError) {
|
||||
throw new Error(
|
||||
`Docker failed to run with the following error message: ${dockerError}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (containerStatusCode !== 0) {
|
||||
throw new Error(
|
||||
`Docker container returned a non-zero exit code (${containerStatusCode})`,
|
||||
);
|
||||
}
|
||||
|
||||
return outDir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Writable } from 'stream';
|
||||
|
||||
export interface RequiredTemplateValues {
|
||||
component_id: string;
|
||||
}
|
||||
@@ -21,6 +23,7 @@ export interface RequiredTemplateValues {
|
||||
export interface TemplaterRunOptions {
|
||||
directory: string;
|
||||
values: RequiredTemplateValues & object;
|
||||
logStream?: Writable;
|
||||
}
|
||||
|
||||
export abstract class TemplaterBase {
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function createRouter(
|
||||
// Run the templater on the mock directory with values from the post body
|
||||
const templatedPath = await templater.run({
|
||||
directory: skeletonPath,
|
||||
values: { component_id: 'test', description: "Something for now" },
|
||||
values: { component_id: 'test' },
|
||||
});
|
||||
|
||||
console.warn(templatedPath);
|
||||
|
||||
Reference in New Issue
Block a user