diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts index 8362a759bf..bddf5a7b25 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.test.ts @@ -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(() => [{ 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\)/, + ); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts index e927108dc4..b612900123 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/cookiecutter.ts @@ -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; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/templater/index.ts b/plugins/scaffolder-backend/src/scaffolder/templater/index.ts index 80570a1c48..0c255354fe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/templater/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/templater/index.ts @@ -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 { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index e71593b6ba..6636fc4b68 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -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);