mirror of https://github.com/docker/docs.git
662 lines
19 KiB
JavaScript
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;
|