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:
@@ -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: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user