feat(scaffolder): error handling, log propagation

Co-authored-by: Ben Lambert <ben@blam.sh>
This commit is contained in:
Ivan Shmidt
2020-06-23 15:30:38 +02:00
parent 12db9bc694
commit e59cac61fd
4 changed files with 88 additions and 9 deletions
@@ -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);