backend-common: Make runDockerContainer more configurable

1. Add optional directories to mount
2. Add optional working directory to use
3. Add optional environment variables to set

Signed-off-by: Himanshu Mishra <himanshu@orkohunter.net>
This commit is contained in:
Himanshu Mishra
2021-03-06 12:42:02 +01:00
parent e67d055dc2
commit 63b8914b82
2 changed files with 57 additions and 45 deletions
+31 -27
View File
@@ -14,15 +14,24 @@
* limitations under the License.
*/
import Docker from 'dockerode';
import fs from 'fs';
import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
import Stream, { PassThrough } from 'stream';
import { runDockerContainer, UserOptions } from './docker';
const mockDocker = new Docker() as jest.Mocked<Docker>;
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
describe('runDockerContainer', () => {
beforeEach(() => {
mockFs({
[rootDir]: {
input: mockFs.directory(),
output: mockFs.directory(),
},
});
jest.spyOn(mockDocker, 'pull').mockImplementation((async (
_image: string,
_something: any,
@@ -42,17 +51,23 @@ describe('runDockerContainer', () => {
.mockResolvedValue(Buffer.from('OK', 'utf-8'));
});
afterEach(() => {
mockFs.restore();
});
const imageName = 'dockerOrg/image';
const args = ['bash', '-c', 'echo test'];
const inputDir = os.tmpdir();
const outputDir = os.tmpdir();
const mountDirs = new Map([
[path.join(rootDir, 'input'), '/input'],
[path.join(rootDir, 'output'), '/output'],
]);
const workingDir = path.join(rootDir, 'input');
const envVars = ['HOME=/tmp', 'LOG_LEVEL=debug'];
it('should pull the docker container', async () => {
await runDockerContainer({
imageName,
args,
inputDir,
outputDir,
dockerClient: mockDocker,
});
@@ -61,14 +76,17 @@ describe('runDockerContainer', () => {
{},
expect.any(Function),
);
expect(mockDocker.run).toHaveBeenCalled();
});
it('should call the dockerClient run command with the correct arguments passed through', async () => {
await runDockerContainer({
imageName,
args,
inputDir,
outputDir,
mountDirs,
envVars,
workingDir,
dockerClient: mockDocker,
});
@@ -77,10 +95,12 @@ describe('runDockerContainer', () => {
args,
expect.any(Stream),
expect.objectContaining({
Env: envVars,
WorkingDir: workingDir,
HostConfig: {
Binds: expect.arrayContaining([
`${await fs.promises.realpath(inputDir)}:/input`,
`${await fs.promises.realpath(outputDir)}:/output`,
`${path.join(rootDir, 'input')}:/input`,
`${path.join(rootDir, 'output')}:/output`,
]),
},
Volumes: {
@@ -95,8 +115,6 @@ describe('runDockerContainer', () => {
await runDockerContainer({
imageName,
args,
inputDir,
outputDir,
dockerClient: mockDocker,
});
@@ -107,8 +125,6 @@ describe('runDockerContainer', () => {
await runDockerContainer({
imageName,
args,
inputDir,
outputDir,
dockerClient: mockDocker,
});
@@ -139,8 +155,6 @@ describe('runDockerContainer', () => {
runDockerContainer({
imageName,
args,
inputDir,
outputDir,
dockerClient: mockDocker,
}),
).rejects.toThrow(/Something went wrong with docker/);
@@ -160,8 +174,6 @@ describe('runDockerContainer', () => {
runDockerContainer({
imageName,
args,
inputDir,
outputDir,
dockerClient: mockDocker,
}),
).rejects.toThrow(new RegExp(`.+: ${dockerError}`));
@@ -173,8 +185,6 @@ describe('runDockerContainer', () => {
await runDockerContainer({
imageName,
args,
inputDir,
outputDir,
logStream,
dockerClient: mockDocker,
});
@@ -185,15 +195,9 @@ describe('runDockerContainer', () => {
logStream,
expect.objectContaining({
HostConfig: {
Binds: expect.arrayContaining([
`${await fs.promises.realpath(inputDir)}:/input`,
`${await fs.promises.realpath(outputDir)}:/output`,
]),
},
Volumes: {
'/input': {},
'/output': {},
Binds: [],
},
Volumes: {},
}),
);
});
+26 -18
View File
@@ -15,7 +15,6 @@
*/
import Docker from 'dockerode';
import fs from 'fs';
import { PassThrough, Writable } from 'stream';
export type UserOptions = {
@@ -26,9 +25,10 @@ export type RunDockerContainerOptions = {
imageName: string;
args: string[];
logStream?: Writable;
inputDir: string;
outputDir: string;
dockerClient: Docker;
mountDirs?: Map<string, string>;
workingDir?: string;
envVars?: string[];
createOptions?: Docker.ContainerCreateOptions;
};
@@ -38,17 +38,20 @@ export type RunDockerContainerOptions = {
* @param options.imageName the image to run
* @param options.args the arguments to pass the container
* @param options.logStream the log streamer to capture log messages
* @param options.inputDir the /input path inside the container
* @param options.outputDir the /output path inside the container
* @param options.dockerClient the dockerClient to use
* @param options.mountDirs A map of host directories to mount on the container.
* Map Key: Path on host machine, Value: Path on Docker container
* @param options.workingDir Working dir in the container
* @param options.envVars Environment variables to set in the container. e.g. ['HOME=/tmp']
*/
export const runDockerContainer = async ({
imageName,
args,
logStream = new PassThrough(),
inputDir,
outputDir,
dockerClient,
mountDirs = new Map(),
workingDir,
envVars = [],
createOptions = {},
}: RunDockerContainerOptions) => {
// Show a better error message when Docker is unavailable.
@@ -80,25 +83,30 @@ export const runDockerContainer = async ({
userOptions.User = `${process.getuid()}:${process.getgid()}`;
}
// Initialize volumes to mount based on mountDirs map
const Volumes: { [T: string]: object } = {};
for (const containerDir of mountDirs.values()) {
Volumes[containerDir] = {};
}
// Create bind volumes
const Binds: string[] = [];
for (const [hostDir, containerDir] of mountDirs.entries()) {
Binds.push(`${hostDir}:${containerDir}`);
}
const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run(
imageName,
args,
logStream,
{
Volumes: { '/output': {}, '/input': {} },
WorkingDir: '/input',
Volumes,
HostConfig: {
Binds: [
// Need to use realpath here as Docker mounting does not like
// symlinks for binding volumes
`${await fs.promises.realpath(outputDir)}:/output`,
`${await fs.promises.realpath(inputDir)}:/input`,
],
Binds,
},
...(workingDir ? { WorkingDir: workingDir } : {}),
Env: envVars,
...userOptions,
// Set the home directory inside the container as something that applications can
// write to, otherwise they will just flop and fail trying to write to /
Env: ['HOME=/tmp'],
...createOptions,
},
);