mirror of https://github.com/docker/docs.git
487 lines
14 KiB
JavaScript
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();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
};
|