docs/src/utils/DockerUtil.js

662 lines
19 KiB
JavaScript

import async from 'async';
import fs from 'fs';
import path from 'path';
import dockerode from 'dockerode';
import _ from 'underscore';
import child_process from 'child_process';
import util from './Util';
import hubUtil from './HubUtil';
import metrics from '../utils/MetricsUtil';
import containerServerActions from '../actions/ContainerServerActions';
import imageServerActions from '../actions/ImageServerActions';
import Promise from 'bluebird';
import rimraf from 'rimraf';
import stream from 'stream';
import JSONStream from 'JSONStream';
import Promise from 'bluebird';
var DockerUtil = {
host: null,
client: null,
placeholders: {},
stream: null,
eventStream: null,
activeContainerName: null,
localImages: null,
setup (ip, name) {
if (!ip && !name) {
throw new Error('Falsy ip or name passed to docker client setup');
}
this.host = ip;
if (ip.indexOf('local') !== -1) {
try {
this.client = new dockerode({socketPath: '/var/run/docker.sock'});
} catch (error) {
throw new Error('Cannot connect to the Docker daemon. Is the daemon running?');
}
} else {
let certDir = path.join(util.home(), '.docker/machine/machines/', name);
if (!fs.existsSync(certDir)) {
throw new Error('Certificate directory does not exist');
}
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'))
});
}
},
async version () {
let version = null;
let maxRetries = 10;
let retries = 0;
let error_message = "";
while (version == null && retries < maxRetries) {
this.client.version((error,data) => {
if (!error) {
version = data.Version;
} else {
error_message = error;
}
retries++;
});
await Promise.delay(1000);
}
if (version == null) {
throw new Error(error_message);
}
return version;
},
init () {
this.placeholders = JSON.parse(localStorage.getItem('placeholders')) || {};
this.fetchAllContainers();
this.fetchAllImages();
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});
});
});
});
},
isDockerRunning () {
try {
child_process.execSync('ps ax | grep "docker daemon" | grep -v grep');
} catch (error) {
throw new Error('Cannot connect to the Docker daemon. The daemon is not running.');
}
},
startContainer (name) {
let container = this.client.getContainer(name);
container.start((error) => {
if (error) {
containerServerActions.error({name, error});
console.log('error starting: %o - %o', 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, () => {});
this.client.getImage(containerData.Image).inspect((error, image) => {
if (error) {
containerServerActions.error({name, error});
return;
}
if (!containerData.HostConfig || (containerData.HostConfig && !containerData.HostConfig.PortBindings)) {
containerData.PublishAllPorts = true;
}
if (image.Config.Cmd) {
containerData.Cmd = image.Config.Cmd;
} else if (!image.Config.Entrypoint) {
containerData.Cmd = 'sh';
}
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);
delete this.placeholders[name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
});
});
});
});
this.fetchAllImages();
},
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) {
console.error(err);
return;
}
async.map(containers, (container, callback) => {
this.client.getContainer(container.Id).inspect((error, container) => {
if (error) {
callback(null, null);
return;
}
container.Name = container.Name.replace('/', '');
callback(null, container);
});
}, (err, containers) => {
containers = containers.filter(c => c !== null);
if (err) {
// TODO: add a global error handler for this
console.error(err);
return;
}
containerServerActions.allUpdated({containers: _.indexBy(containers.concat(_.values(this.placeholders)), 'Name')});
});
});
},
fetchAllImages () {
this.client.listImages((err, list) => {
if (err) {
imageServerActions.error(err);
} else {
this.localImages = list;
imageServerActions.updated(list);
}
});
},
removeImage (selectedRepoTag) {
this.localImages.some((image) => {
image.RepoTags.map(repoTag => {
if (repoTag === selectedRepoTag) {
this.client.getImage(selectedRepoTag).remove({'force': true}, (err, data) => {
if (err) {
console.error(err);
imageServerActions.error(err);
} else {
imageServerActions.destroyed(data);
}
});
return true;
}
});
});
},
run (name, repository, tag, local = false) {
tag = tag || 'latest';
let imageName = repository + ':' + tag;
let placeholderData = {
Id: util.randomId(),
Name: name,
Image: imageName,
Config: {
Image: imageName
},
Tty: true,
OpenStdin: true,
State: {
Downloading: true
}
};
containerServerActions.added({container: placeholderData});
this.placeholders[name] = placeholderData;
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
if (local) {
this.createContainer(name, {Image: imageName, Tty: true, OpenStdin: true});
} else {
this.pullImage(repository, tag, error => {
if (error) {
containerServerActions.error({name, error});
return;
}
if (!this.placeholders[name]) {
return;
}
this.createContainer(name, {Image: imageName, Tty: true, OpenStdin: true});
},
// 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;
}
if ((!existingData.Tty || !existingData.OpenStdin) && existingData.Config && (existingData.Config.Tty || existingData.Config.OpenStdin)) {
existingData.Tty = existingData.Config.Tty;
existingData.OpenStdin = existingData.Config.OpenStdin;
}
data.Mounts = data.Mounts || existingData.Mounts;
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 = util.windowsToLinuxPath(path.join(util.home(), util.documents(), 'Kitematic', name));
var newPath = util.windowsToLinuxPath(path.join(util.home(), util.documents(), '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);
}
container.Mounts.forEach(m => {
m.Source = m.Source.replace(oldPath, newPath);
});
this.updateContainer(newName, {Mounts: container.Mounts});
rimraf(oldPath, () => {});
});
});
});
},
restart (name) {
this.client.getContainer(name).stop({t: 5}, stopError => {
if (stopError && stopError.statusCode !== 304) {
containerServerActions.error({name, stopError});
return;
}
this.client.getContainer(name).start(startError => {
if (startError && startError.statusCode !== 304) {
containerServerActions.error({name, startError});
return;
}
this.fetchContainer(name);
});
});
},
stop (name) {
this.client.getContainer(name).stop({t: 5}, 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 () {
container.remove(function (error) {
if (error) {
containerServerActions.error({name, error});
return;
}
containerServerActions.destroyed({id: name});
var volumePath = path.join(util.home(), 'Kitematic', name);
if (fs.existsSync(volumePath)) {
rimraf(volumePath, () => {});
}
});
});
});
},
active (name) {
this.detachLog();
this.activeContainerName = name;
if (name) {
this.logs();
}
},
logs () {
if (!this.activeContainerName) {
return;
}
this.client.getContainer(this.activeContainerName).logs({
stdout: true,
stderr: true,
tail: 1000,
follow: false,
timestamps: 1
}, (err, logStream) => {
if (err) {
// socket hang up can be captured
console.error(err);
return;
}
let logs = '';
logStream.setEncoding('utf8');
logStream.on('data', chunk => logs += chunk);
logStream.on('end', () => {
containerServerActions.logs({name: this.activeContainerName, logs});
this.attach();
});
});
},
attach () {
if (!this.activeContainerName) {
return;
}
this.client.getContainer(this.activeContainerName).logs({
stdout: true,
stderr: true,
tail: 0,
follow: true,
timestamps: 1
}, (err, logStream) => {
if (err) {
// Socket hang up also can be found here
console.error(err);
return;
}
this.detachLog()
this.stream = logStream;
let timeout = null;
let batch = '';
logStream.setEncoding('utf8');
logStream.on('data', (chunk) => {
batch += chunk;
if (!timeout) {
timeout = setTimeout(() => {
containerServerActions.log({name: this.activeContainerName, entry: batch});
timeout = null;
batch = '';
}, 16);
}
});
});
},
detachLog() {
if (this.stream) {
this.stream.destroy();
this.stream = null;
}
},
detachEvent() {
if (this.eventStream) {
this.eventStream.destroy();
this.eventStream = null;
}
},
listen () {
this.detachEvent()
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' || data.status === 'attach') {
this.fetchAllImages();
}
if (data.status === 'destroy') {
containerServerActions.destroyed({id: data.id});
this.detachLog()
} else if (data.status === 'kill') {
containerServerActions.kill({id: data.id});
this.detachLog()
} else if (data.status === 'stop') {
containerServerActions.stopped({id: data.id});
this.detachLog()
} else if (data.status === 'create') {
this.logs();
this.fetchContainer(data.id);
} else if (data.status === 'start') {
this.attach();
this.fetchContainer(data.id);
} else if (data.id) {
this.fetchContainer(data.id);
}
});
this.eventStream = stream;
});
},
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) {
console.log('Err: %o', 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
let error = null;
// data is associated with one layer only (can be identified with id)
stream.pipe(JSONStream.parse()).on('data', data => {
if (data.error) {
error = 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(error);
});
});
}
};
module.exports = DockerUtil;