docs/src/ContainerStore.js

420 lines
13 KiB
JavaScript

var _ = require('underscore');
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var path = require('path');
var assign = require('object-assign');
var docker = require('./Docker');
var metrics = require('./Metrics');
var registry = require('./Registry');
var LogStore = require('./LogStore');
var _placeholders = {};
var _containers = {};
var _progress = {};
var _muted = {};
var _blocked = {};
var ContainerStore = assign(Object.create(EventEmitter.prototype), {
CLIENT_CONTAINER_EVENT: 'client_container_event',
SERVER_CONTAINER_EVENT: 'server_container_event',
SERVER_PROGRESS_EVENT: 'server_progress_event',
_pullImage: function (repository, tag, callback, progressCallback, blockedCallback) {
registry.layers(repository, tag, (err, layerSizes) => {
// TODO: Support v2 registry API
// TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
// Use the per-layer pull progress % to update the total progress.
docker.client().listImages({all: 1}, (err, images) => {
images = images || [];
var existingIds = new Set(images.map(function (image) {
return image.Id.slice(0, 12);
}));
var layersToDownload = layerSizes.filter(function (layerSize) {
return !existingIds.has(layerSize.Id);
});
var totalBytes = layersToDownload.map(function (s) { return s.size; }).reduce(function (pv, sv) { return pv + sv; }, 0);
docker.client().pull(repository + ':' + tag, (err, stream) => {
stream.setEncoding('utf8');
var layerProgress = layersToDownload.reduce(function (r, layer) {
if (_.findWhere(images, {Id: layer.Id})) {
r[layer.Id] = 100;
} else {
r[layer.Id] = 0;
}
return r;
}, {});
stream.on('data', str => {
var data = JSON.parse(str);
console.log(data);
if (data.status === 'Pulling dependent layers') {
blockedCallback();
return;
}
if (data.status === 'Already exists') {
layerProgress[data.id] = 1;
} else if (data.status === 'Downloading') {
var current = data.progressDetail.current;
var total = data.progressDetail.total;
var layerFraction = current / total;
layerProgress[data.id] = layerFraction;
}
var chunks = layersToDownload.map(function (s) {
return layerProgress[s.Id] * s.size;
});
var totalReceived = chunks.reduce(function (pv, sv) {
return pv + sv;
}, 0);
var totalProgress = totalReceived / totalBytes;
progressCallback(totalProgress);
});
stream.on('end', function () {
callback();
});
});
});
});
},
_createContainer: function (name, containerData, callback) {
var existing = docker.client().getContainer(name);
var self = this;
if (!containerData.name && containerData.Name) {
containerData.name = containerData.Name;
} else if (!containerData.name) {
containerData.name = name;
}
if (containerData.Config && containerData.Config.Image) {
containerData.Image = containerData.Config.Image;
}
existing.kill(function () {
existing.remove(function () {
docker.client().getImage(containerData.Image).inspect(function (err, data) {
if (err) {
callback(err);
return;
}
var binds = containerData.Binds || [];
if (data.Config.Volumes) {
_.each(data.Config.Volumes, function (value, key) {
var existingBind = _.find(binds, b => {
return b.indexOf(':' + key) !== -1;
});
if (!existingBind) {
binds.push(path.join(process.env.HOME, 'Kitematic', containerData.name, key)+ ':' + key);
}
});
}
docker.client().createContainer(containerData, function (err, container) {
if (err) {
callback(err, null);
return;
}
if (containerData.State && !containerData.State.Running) {
self.fetchContainer(containerData.name, callback);
} else {
container.start({
PublishAllPorts: true,
Binds: binds
}, function (err) {
if (err) {
callback(err);
return;
}
self.fetchContainer(containerData.name, callback);
});
}
});
});
});
});
},
_generateName: function (repository) {
var base = _.last(repository.split('/'));
var count = 1;
var name = base;
while (true) {
var exists = _.findWhere(_.values(_containers), {Name: name}) || _.findWhere(_.values(_containers), {Name: name});
if (!exists) {
return name;
} else {
count++;
name = base + '-' + count;
}
}
},
_resumePulling: function () {
var downloading = _.filter(_.values(this.containers()), function (container) {
return container.State.Downloading;
});
// Recover any pulls that were happening
var self = this;
downloading.forEach(function (container) {
_progress[container.Name] = 99;
docker.client().pull(container.Config.Image, function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function () {});
stream.on('end', function () {
delete _placeholders[container.Name];
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
self._createContainer(container.Name, {Image: container.Config.Image}, function () {
self.emit(self.SERVER_PROGRESS_EVENT, container.Name);
self.emit(self.CLIENT_CONTAINER_EVENT, container.Name);
});
});
});
});
},
_startListeningToEvents: function () {
docker.client().getEvents(function (err, stream) {
if (stream) {
stream.setEncoding('utf8');
stream.on('data', this._dockerEvent.bind(this));
}
}.bind(this));
},
_dockerEvent: function (json) {
var data = JSON.parse(json);
console.log(data);
// If the event is delete, remove the container
if (data.status === 'destroy') {
var container = _.findWhere(_.values(_containers), {Id: data.id});
if (container) {
delete _containers[container.Name];
if (!_muted[container.Name]) {
this.emit(this.SERVER_CONTAINER_EVENT, container.Name, data.status);
}
} else {
this.emit(this.SERVER_CONTAINER_EVENT, data.status);
}
} else {
this.fetchContainer(data.id, function (err) {
if (err) {
return;
}
var container = _.findWhere(_.values(_containers), {Id: data.id});
if (!container || _muted[container.Name]) {
return;
}
this.emit(this.SERVER_CONTAINER_EVENT, container ? container.Name : null, data.status);
}.bind(this));
}
},
init: function (callback) {
// TODO: Load cached data from db on loading
this.fetchAllContainers(function (err) {
if (err) {
callback(err);
return;
} else {
callback();
}
var placeholderData = JSON.parse(localStorage.getItem('store.placeholders'));
if (placeholderData) {
_placeholders = _.omit(placeholderData, _.keys(_containers));
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
}
this.emit(this.CLIENT_CONTAINER_EVENT);
this._resumePulling();
this._startListeningToEvents();
}.bind(this));
},
fetchContainer: function (id, callback) {
docker.client().getContainer(id).inspect(function (err, container) {
if (err) {
callback(err);
} else {
if (container.Config.Image === container.Image.slice(0, 12) || container.Config.Image === container.Image) {
callback();
return;
}
// Fix leading slash in container names
container.Name = container.Name.replace('/', '');
_containers[container.Name] = container;
callback(null, container);
}
}.bind(this));
},
fetchAllContainers: function (callback) {
var self = this;
docker.client().listContainers({all: true}, function (err, containers) {
if (err) {
callback(err);
return;
}
var names = new Set(_.map(containers, container => container.Names[0].replace('/', '')));
_.each(_.keys(_containers), name => {
if (!names.has(name)) {
delete _containers[name];
}
});
async.each(containers, function (container, callback) {
self.fetchContainer(container.Id, function (err) {
callback(err);
});
}, function (err) {
callback(err);
});
});
},
create: function (repository, tag, callback) {
tag = tag || 'latest';
var imageName = repository + ':' + tag;
var containerName = this._generateName(repository);
_placeholders[containerName] = {
Name: containerName,
Image: imageName,
Config: {
Image: imageName,
},
State: {
Downloading: true
}
};
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
this.emit(this.CLIENT_CONTAINER_EVENT, containerName, 'create');
_muted[containerName] = true;
_progress[containerName] = 0;
this._pullImage(repository, tag, () => {
_blocked[containerName] = false;
if (!_placeholders[containerName]) {
return;
}
delete _placeholders[containerName];
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
this._createContainer(containerName, {Image: imageName}, () => {
metrics.track('Container Finished Creating');
delete _progress[containerName];
_muted[containerName] = false;
this.emit(this.CLIENT_CONTAINER_EVENT, containerName);
});
}, progress => {
_blocked[containerName] = false;
_progress[containerName] = progress;
this.emit(this.SERVER_PROGRESS_EVENT, containerName);
}, () => {
_blocked[containerName] = true;
this.emit(this.SERVER_PROGRESS_EVENT, containerName);
});
callback(null, containerName);
},
updateContainer: function (name, data, callback) {
_muted[name] = true;
if (!data.name) {
data.name = data.Name;
}
if (name !== data.name) {
LogStore.rename(name, data.name);
}
var fullData = assign(_containers[name], data);
this._createContainer(name, fullData, function (err) {
_muted[name] = false;
this.emit(this.CLIENT_CONTAINER_EVENT, name);
callback(err);
}.bind(this));
},
rename: function (name, newName, callback) {
LogStore.rename(name, newName);
docker.client().getContainer(name).rename({name: newName}, err => {
if (err && err.statusCode !== 204) {
callback(err);
return;
}
this.fetchAllContainers(err => {
this.emit(this.CLIENT_CONTAINER_EVENT);
callback(err);
});
});
},
restart: function (name, callback) {
var container = docker.client().getContainer(name);
container.restart(function (err) {
callback(err);
});
},
remove: function (name, callback) {
if (_placeholders[name]) {
delete _placeholders[name];
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
this.emit(this.CLIENT_CONTAINER_EVENT, name, 'destroy');
callback();
return;
}
var container = docker.client().getContainer(name);
if (_containers[name].State.Paused) {
container.unpause(function (err) {
if (err) {
callback(err);
return;
} else {
container.kill(function (err) {
if (err) {
callback(err);
return;
} else {
container.remove(function (err) {
if (err) {
callback(err);
return;
}
});
}
});
}
});
} else {
container.kill(function (err) {
if (err) {
callback(err);
return;
} else {
container.remove(function (err) {
callback(err);
});
}
});
}
},
containers: function() {
return _.extend(_.clone(_containers), _placeholders);
},
container: function (name) {
return this.containers()[name];
},
sorted: function () {
return _.values(this.containers()).sort(function (a, b) {
if (a.State.Downloading && !b.State.Downloading) {
return -1;
} else if (!a.State.Downloading && b.State.Downloading) {
return 1;
} else {
if (a.State.Running && !b.State.Running) {
return -1;
} else if (!a.State.Running && b.State.Running) {
return 1;
} else {
return a.Name.localeCompare(b.Name);
}
}
});
},
progress: function (name) {
return _progress[name];
},
blocked: function (name) {
return !!_blocked[name];
}
});
module.exports = ContainerStore;