docs/src/utils/DockerUtil.js

487 lines
14 KiB
JavaScript

import async from 'async';
import fs from 'fs';
import path from 'path';
import dockerode from 'dockerode';
import _ from 'underscore';
import util from './Util';
import hubUtil from './HubUtil';
import metrics from '../utils/MetricsUtil';
import containerServerActions from '../actions/ContainerServerActions';
import Promise from 'bluebird';
import rimraf from 'rimraf';
export default {
host: null,
client: null,
placeholders: {},
setup (ip, name) {
if (!ip || !name) {
throw new Error('Falsy ip or name passed to docker client setup');
}
let certDir = path.join(util.home(), '.docker/machine/machines/', name);
if (!fs.existsSync(certDir)) {
throw new Error('Certificate directory does not exist');
}
console.log(ip);
this.host = ip;
this.client = new dockerode({
protocol: 'https',
host: ip,
port: 2376,
ca: fs.readFileSync(path.join(certDir, 'ca.pem')),
cert: fs.readFileSync(path.join(certDir, 'cert.pem')),
key: fs.readFileSync(path.join(certDir, 'key.pem'))
});
},
init () {
this.placeholders = JSON.parse(localStorage.getItem('placeholders')) || {};
this.fetchAllContainers();
this.listen();
// Resume pulling containers that were previously being pulled
_.each(_.values(this.placeholders), container => {
containerServerActions.added({container});
this.client.pull(container.Config.Image, (error, stream) => {
if (error) {
containerServerActions.error({name: container.Name, error});
return;
}
stream.setEncoding('utf8');
stream.on('data', function () {});
stream.on('end', () => {
if (!this.placeholders[container.Name]) {
return;
}
delete this.placeholders[container.Name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
this.createContainer(container.Name, {Image: container.Config.Image});
});
});
});
},
startContainer (name, containerData) {
let startopts = {
Binds: containerData.Binds || []
};
if (containerData.NetworkSettings && containerData.NetworkSettings.Ports) {
startopts.PortBindings = containerData.NetworkSettings.Ports;
} else if (containerData.HostConfig && containerData.HostConfig.PortBindings) {
startopts.PortBindings = containerData.HostConfig.PortBindings;
} else {
startopts.PublishAllPorts = true;
}
let container = this.client.getContainer(name);
container.start(startopts, (error) => {
if (error) {
containerServerActions.error({name, error});
return;
}
containerServerActions.started({name, error});
this.fetchContainer(name);
});
},
createContainer (name, containerData) {
containerData.name = containerData.Name || name;
if (containerData.Config && containerData.Config.Image) {
containerData.Image = containerData.Config.Image;
}
if (!containerData.Env && containerData.Config && containerData.Config.Env) {
containerData.Env = containerData.Config.Env;
}
containerData.Volumes = _.mapObject(containerData.Volumes, () => {return {};});
let existing = this.client.getContainer(name);
existing.kill(() => {
existing.remove(() => {
this.client.createContainer(containerData, (error) => {
if (error) {
containerServerActions.error({name, error});
return;
}
metrics.track('Container Finished Creating');
this.startContainer(name, containerData);
});
});
});
},
fetchContainer (id) {
this.client.getContainer(id).inspect((error, container) => {
if (error) {
containerServerActions.error({name: id, error});
} else {
container.Name = container.Name.replace('/', '');
containerServerActions.updated({container});
}
});
},
fetchAllContainers () {
this.client.listContainers({all: true}, (err, containers) => {
if (err) {
return;
}
async.map(containers, (container, callback) => {
this.client.getContainer(container.Id).inspect((error, container) => {
container.Name = container.Name.replace('/', '');
callback(null, container);
});
}, (err, containers) => {
if (err) {
// TODO: add a global error handler for this
return;
}
containerServerActions.allUpdated({containers: _.indexBy(containers.concat(_.values(this.placeholders)), 'Name')});
});
});
},
run (name, repository, tag) {
tag = tag || 'latest';
let imageName = repository + ':' + tag;
let placeholderData = {
Id: util.randomId(),
Name: name,
Image: imageName,
Config: {
Image: imageName,
},
State: {
Downloading: true
}
};
containerServerActions.added({container: placeholderData});
this.placeholders[name] = placeholderData;
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
this.pullImage(repository, tag, error => {
if (error) {
containerServerActions.error({name, error});
return;
}
if (!this.placeholders[name]) {
return;
}
delete this.placeholders[name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
this.createContainer(name, {Image: imageName});
},
// progress is actually the progression PER LAYER (combined in columns)
// not total because it's not accurate enough
progress => {
containerServerActions.progress({name, progress});
},
() => {
containerServerActions.waiting({name, waiting: true});
});
},
updateContainer (name, data) {
let existing = this.client.getContainer(name);
existing.inspect((error, existingData) => {
if (error) {
containerServerActions.error({name, error});
return;
}
if (existingData.Config && existingData.Config.Image) {
existingData.Image = existingData.Config.Image;
}
if (!existingData.Env && existingData.Config && existingData.Config.Env) {
existingData.Env = existingData.Config.Env;
}
var fullData = _.extend(existingData, data);
this.createContainer(name, fullData);
});
},
rename (name, newName) {
this.client.getContainer(name).rename({name: newName}, error => {
if (error && error.statusCode !== 204) {
containerServerActions.error({name, error});
return;
}
var oldPath = path.join(util.home(), 'Kitematic', name);
var newPath = path.join(util.home(), 'Kitematic', newName);
this.client.getContainer(newName).inspect((error, container) => {
if (error) {
// TODO: handle error
containerServerActions.error({newName, error});
}
rimraf(newPath, () => {
if (fs.existsSync(oldPath)) {
fs.renameSync(oldPath, newPath);
}
var binds = _.pairs(container.Volumes).map(function (pair) {
return pair[1] + ':' + pair[0];
});
var newBinds = binds.map(b => {
return b.replace(path.join(util.home(), 'Kitematic', name), path.join(util.home(), 'Kitematic', newName));
});
this.updateContainer(newName, {Binds: newBinds});
rimraf(oldPath, () => {});
});
});
});
},
restart (name) {
let container = this.client.getContainer(name);
container.stop(error => {
if (error && error.statusCode !== 304) {
containerServerActions.error({name, error});
return;
}
container.inspect((error, data) => {
if (error) {
containerServerActions.error({name, error});
}
this.startContainer(name, data);
});
});
},
stop (name) {
this.client.getContainer(name).stop(error => {
if (error && error.statusCode !== 304) {
containerServerActions.error({name, error});
return;
}
this.fetchContainer(name);
});
},
start (name) {
this.client.getContainer(name).start(error => {
if (error && error.statusCode !== 304) {
containerServerActions.error({name, error});
return;
}
this.fetchContainer(name);
});
},
destroy (name) {
if (this.placeholders[name]) {
containerServerActions.destroyed({id: name});
delete this.placeholders[name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
return;
}
let container = this.client.getContainer(name);
container.unpause(function () {
container.kill(function (error) {
if (error) {
containerServerActions.error({name, error});
return;
}
container.remove(function () {
containerServerActions.destroyed({id: name});
var volumePath = path.join(util.home(), 'Kitematic', name);
if (fs.existsSync(volumePath)) {
rimraf(volumePath, () => {});
}
});
});
});
},
listen () {
this.client.getEvents((error, stream) => {
if (error || !stream) {
// TODO: Add app-wide error handler
return;
}
stream.setEncoding('utf8');
stream.on('data', json => {
let data = JSON.parse(json);
if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete') {
return;
}
if (data.status === 'destroy') {
containerServerActions.destroyed({name: data.id});
} else if (data.status === 'create') {
this.fetchAllContainers();
} else {
this.fetchContainer(data.id);
}
});
});
},
pullImage (repository, tag, callback, progressCallback, blockedCallback) {
let opts = {}, config = hubUtil.config();
if (!hubUtil.config()) {
opts = {};
} else {
let [username, password] = hubUtil.creds(config);
opts = {
authconfig: {
username,
password,
auth: ''
}
};
}
this.client.pull(repository + ':' + tag, opts, (err, stream) => {
if (err) {
callback(err);
return;
}
stream.setEncoding('utf8');
// scheduled to inform about progression at given interval
let tick = null;
let layerProgress = {};
// Split the loading in a few columns for more feedback
let columns = {};
columns.amount = 4; // arbitrary
columns.toFill = 0; // the current column index, waiting for layer IDs to be displayed
// data is associated with one layer only (can be identified with id)
stream.on('data', str => {
var data = JSON.parse(str);
if (data.error) {
callback(data.error);
return;
}
if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) {
blockedCallback();
return;
}
if (data.status === 'Pulling fs layer') {
layerProgress[data.id] = {
current: 0,
total: 1
};
} else if (data.status === 'Downloading') {
if (!columns.progress) {
columns.progress = []; // layerIDs, nbLayers, maxLayers, progress value
let layersToLoad = _.keys(layerProgress).length;
let layersPerColumn = Math.floor(layersToLoad / columns.amount);
let leftOverLayers = layersToLoad % columns.amount;
for (let i = 0; i < columns.amount; i++) {
let layerAmount = layersPerColumn;
if (i < leftOverLayers) {
layerAmount += 1;
}
columns.progress[i] = {layerIDs: [], nbLayers:0 , maxLayers: layerAmount, value: 0.0};
}
}
layerProgress[data.id].current = data.progressDetail.current;
layerProgress[data.id].total = data.progressDetail.total;
// Assign to a column if not done yet
if (!layerProgress[data.id].column) {
// test if we can still add layers to that column
if (columns.progress[columns.toFill].nbLayers === columns.progress[columns.toFill].maxLayers && columns.toFill < columns.amount - 1) {
columns.toFill++;
}
layerProgress[data.id].column = columns.toFill;
columns.progress[columns.toFill].layerIDs.push(data.id);
columns.progress[columns.toFill].nbLayers++;
}
if (!tick) {
tick = setTimeout(() => {
clearInterval(tick);
tick = null;
for (let i = 0; i < columns.amount; i++) {
columns.progress[i].value = 0.0;
if (columns.progress[i].nbLayers > 0) {
let layer;
let totalSum = 0;
let currentSum = 0;
for (let j = 0; j < columns.progress[i].nbLayers; j++) {
layer = layerProgress[columns.progress[i].layerIDs[j]];
totalSum += layer.total;
currentSum += layer.current;
}
if (totalSum > 0) {
columns.progress[i].value = Math.min(100.0 * currentSum / totalSum, 100);
} else {
columns.progress[i].value = 0.0;
}
}
}
progressCallback(columns);
}, 16);
}
}
});
stream.on('end', function () {
callback();
});
});
},
// TODO: move this to machine health checks
waitForConnection (tries, delay) {
tries = tries || 10;
delay = delay || 1000;
let tryCount = 1, connected = false;
return new Promise((resolve, reject) => {
async.until(() => connected, callback => {
this.client.listContainers(error => {
if (error) {
if (tryCount > tries) {
callback(Error('Cannot connect to the Docker Engine. Either the VM is not responding or the connection may be blocked (VPN or Proxy): ' + error.message));
} else {
tryCount += 1;
setTimeout(callback, delay);
}
} else {
connected = true;
callback();
}
});
}, error => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
};