Skip to content

Commit

Permalink
Merge 354b893 into de48979
Browse files Browse the repository at this point in the history
  • Loading branch information
tlaanemaa committed May 5, 2019
2 parents de48979 + 354b893 commit 4ed9156
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 3 deletions.
32 changes: 30 additions & 2 deletions __mocks__/dockerode.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
const Docker = jest.genMockFromModule('dockerode');
const Dockerode = jest.genMockFromModule('dockerode');

// Mock constructor
class Docker extends Dockerode {
constructor(...args) {
super(...args);
this.modem = {
followProgress: (_, onFinished, onProgress) => {
onProgress(...Docker.mockOnProgressArgs);
onFinished(...Docker.mockOnFinishedArgs);
},
};
}
}

Docker.prototype.listContainers = () => Promise.resolve([
{ Id: 1 },
{ Id: 2 },
{ Id: 3 },
]);

Docker.mockOnFinishedArgs = [null, 'orange'];
Docker.mockOnProgressArgs = [{ status: 'banana', progressDetail: { current: 5, total: 100 } }];

Docker.mockInspection = {
Name: 'banana',
State: {
Expand All @@ -15,6 +31,9 @@ Docker.mockInspection = {
{ Name: 'mount1', Destination: 'dest1', Type: 'volume' },
{ Name: 'mount2', Destination: 'dest2', Type: 'volume' },
],
Config: {
Image: 'mock/image',
},
};

Docker.mockContainer = {
Expand All @@ -24,9 +43,18 @@ Docker.mockContainer = {
wait: () => Promise.resolve(),
};

Docker.prototype.getContainer = () => Docker.mockContainer;
Docker.mockImage = {
inspect: () => {
throw new Error('Image does not exist');
},
};

Docker.prototype.getContainer = () => Docker.mockContainer;
Docker.prototype.getImage = () => Docker.mockImage;
Docker.prototype.run = jest.fn().mockResolvedValue();
Docker.prototype.createContainer = jest.fn().mockResolvedValue(Docker.mockContainer);
Docker.prototype.pull = jest.fn().mockImplementation((name, callback) => {
callback(null, 'stream');
});

module.exports = Docker;
4 changes: 3 additions & 1 deletion __mocks__/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ const mockInspectFile = `
"Type": "volume"
}
],
"Config": {},
"Config": {
"Image": "mock/image"
},
"HostConfig": {
"NetworkMode": "mockMode"
},
Expand Down
79 changes: 79 additions & 0 deletions src/modules/docker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const Docker = require('dockerode');
const { parseRepositoryTag } = require('dockerode/lib/util');
const createLimiter = require('limit-async');
const { socketPath, onlyContainers, onlyVolumes } = require('./options');
const folderStructure = require('./folderStructure');
Expand All @@ -8,6 +9,7 @@ const {
saveInspect,
loadInspect,
getVolumeFilesSync,
round,
} = require('./utils');

// Name of the image we will use for volume operations
Expand All @@ -28,14 +30,74 @@ const operateOnContainers = onlyContainers || (!onlyContainers && !onlyVolumes);
const operateOnVolumes = onlyVolumes || (!onlyVolumes && !onlyContainers);

// If we know that we will operate on volumes, get all volume files in advance
// Also initialize a variable for pulling the volume operations image if needed
const volumeFiles = operateOnVolumes ? getVolumeFilesSync() : [];
let volumeImagePromise = Promise.resolve();

// Get all containers
const getContainers = async (all = true) => {
const containers = await docker.listContainers({ all });
return containers.map(container => container.Id);
};

// Helper to check if an image exists locally
const imageExists = async (name) => {
try {
const image = docker.getImage(name);
await image.inspect();
return true;
} catch (e) {
return false;
}
};

// Helper to pull an image and log
const pullImage = name => new Promise((resolve, reject) => {
// TODO: Remove this once dockerode supports default tags
const imageName = parseRepositoryTag(name).tag ? name : `${name}:latest`;
// eslint-disable-next-line no-console
console.log(`Pulling image: ${imageName}`);
docker.pull(
imageName,
(err, stream) => {
if (err) {
reject(err);
return;
}

const onProgress = (event) => {
let progress = '';
if (
event.progressDetail
&& event.progressDetail.current
&& event.progressDetail.total
) {
progress = ` (${round(event.progressDetail.current / event.progressDetail.total * 100, 2)}%)`;
}

// eslint-disable-next-line no-console
console.log(event.status + progress);
};

const onFinished = (finalErr, finalStream) => {
if (finalErr) {
reject(finalErr);
return;
}

resolve(finalStream);
};
docker.modem.followProgress(stream, onFinished, onProgress);
},
);
});

// Helper to only pull if it doesn't exist
const ensureImageExists = async (name) => {
const exists = await imageExists(name);
if (!exists) await pullImage(name);
};

// Helper to start container and log
const startContainer = (container) => {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -67,8 +129,12 @@ const backupVolume = volumeLimit((containerName, volumeName, mountPoint) => dock

// Back up single container by id
const backupContainer = containerLimit(async (id) => {
// Wait for the volume operations image to be downloaded before proceeding
await volumeImagePromise;

// eslint-disable-next-line no-console
console.log(`== Backing up container: ${id} ==`);

const container = docker.getContainer(id);
const inspect = await container.inspect();
const name = formatContainerName(inspect.Name);
Expand Down Expand Up @@ -128,13 +194,19 @@ const restoreVolume = volumeLimit((containerName, tarName, mountPoint) => docker

// Restore (create) container by id
const restoreContainer = containerLimit(async (name) => {
// Wait for the volume operations image to be downloaded before proceeding
await volumeImagePromise;

// eslint-disable-next-line no-console
console.log(`== Restoring container: ${name} ==`);
const backupInspect = await loadInspect(name);

// Restore container
let container = null;
if (operateOnContainers) {
// Pull the image if it doesn't exist
await ensureImageExists(backupInspect.Config.Image);

// eslint-disable-next-line no-console
console.log('Creating container...');
container = await docker.createContainer(inspect2Config(backupInspect));
Expand Down Expand Up @@ -188,9 +260,16 @@ const restoreContainer = containerLimit(async (name) => {
return true;
});

// Start pulling the volume operations image if we plan to work on volumes
if (operateOnVolumes) {
volumeImagePromise = ensureImageExists(volumeOperationsImage);
}

// Exports
module.exports = {
getContainers,
backupContainer,
restoreContainer,
pullImage,
ensureImageExists,
};
7 changes: 7 additions & 0 deletions src/modules/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ const logAndReturnErrors = func => async (...args) => {
}
};

// Rounding function
const round = (num, decimalPoints = 0) => {
const multiplier = 10 ** decimalPoints;
return Math.round(num * multiplier) / multiplier;
};

// Exports
module.exports = {
formatContainerName,
Expand All @@ -58,4 +64,5 @@ module.exports = {
saveInspect,
getVolumeFilesSync,
logAndReturnErrors,
round,
};
66 changes: 66 additions & 0 deletions test/modules/docker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ describe('restoreContainer', () => {
NetworkingConfig: {
EndpointsConfig: {},
},
Image: 'mock/image',
Volumes: {
dest1: {},
dest2: {},
Expand Down Expand Up @@ -201,4 +202,69 @@ describe('restoreContainer', () => {
expect(dockerode.prototype.run).toHaveBeenCalledTimes(0);
expect(dockerode.mockContainer.stop).toHaveBeenCalledTimes(0);
});

it('should throw error if pull fails', async () => {
expect.assertions(1);
const mockError = new Error('Panic!');
const dockerode = require('dockerode');
dockerode.prototype.pull = jest.fn().mockImplementation((_, callback) => callback(mockError));
const docker = require('../../src/modules/docker');

try {
await docker.restoreContainer('orange');
} catch (e) {
expect(e).toBe(mockError);
}
});

it('should throw error if pull onFinished callback fails', async () => {
expect.assertions(1);
const mockError = new Error('Panic!');
const dockerode = require('dockerode');
dockerode.mockOnFinishedArgs = [mockError];
const docker = require('../../src/modules/docker');

try {
await docker.restoreContainer('orange');
} catch (e) {
expect(e).toBe(mockError);
}
});
});

describe('pullImage', () => {
it('should not add tag if one is present', () => {
const dockerode = require('dockerode');
dockerode.prototype.pull = jest.fn();
const docker = require('../../src/modules/docker');

docker.pullImage('mock/image:10');

expect(dockerode.prototype.pull).toHaveBeenCalledWith('mock/image:10', expect.any(Function));
});

it('should not add progress if there isn\'t any', async () => {
const dockerode = require('dockerode');
dockerode.mockOnProgressArgs = [{ status: 'Pulling bananas' }];
global.console.log = jest.fn();
const docker = require('../../src/modules/docker');

await docker.pullImage('banana');

expect(global.console.log).toHaveBeenCalledWith('Pulling bananas');
});
});

describe('ensureImageExists', () => {
it('should not pull if the image already exists', async () => {
const dockerode = require('dockerode');
dockerode.mockImage.inspect = () => 'mockImageInspect';
const docker = require('../../src/modules/docker');
docker.pullImage = jest.fn();

const result = await docker.ensureImageExists('banana');

expect(result).toEqual(undefined);
expect(docker.pullImage).toHaveBeenCalledTimes(0);
});
});
13 changes: 13 additions & 0 deletions test/modules/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
jest.mock('fs');
jest.mock('../../src/modules/options.js');
const { round } = require('../../src/modules/utils');

describe('round', () => {
it('should round to given decimals', () => {
expect(round(1.234567, 2)).toBe(1.23);
});

it('should round to 0 decimals by default', () => {
expect(round(1.234567)).toBe(1);
});
});

0 comments on commit 4ed9156

Please sign in to comment.