Image sizes & total progress. Restructure events

This commit is contained in:
Jeffrey Morgan 2015-01-20 21:49:14 -05:00
parent b2d319edcb
commit 183517b054
6 changed files with 300 additions and 237 deletions

View File

@ -19,6 +19,7 @@ var ContainerDetails = React.createClass({
};
},
componentWillReceiveProps: function () {
console.log('props');
this.update();
var self = this;
var logs = [];
@ -60,63 +61,25 @@ var ContainerDetails = React.createClass({
});
});
});
},
componentWillMount: function () {
this.update();
var self = this;
var logs = [];
var index = 0;
docker.client().getContainer(this.getParams().name).logs({
follow: false,
stdout: true,
timestamps: true
}, function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (buf) {
// Every other message is a header
if (index % 2 === 1) {
var time = buf.substr(0,buf.indexOf(' '));
var msg = buf.substr(buf.indexOf(' ')+1);
logs.push(convert.toHtml(self._escapeHTML(msg)));
}
index += 1;
});
stream.on('end', function (buf) {
self.setState({logs: logs});
docker.client().getContainer(self.getParams().name).logs({
follow: true,
stdout: true,
timestamps: true,
tail: 0
}, function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (buf) {
// Every other message is a header
if (index % 2 === 1) {
var time = buf.substr(0,buf.indexOf(' '));
var msg = buf.substr(buf.indexOf(' ')+1);
logs.push(convert.toHtml(self._escapeHTML(msg)));
self.setState({logs: logs});
}
index += 1;
});
});
});
});
},
componentDidMount: function () {
var containerName = this.getParams().name;
ContainerStore.addChangeListener(containerName, this.update);
ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.update);
ContainerStore.addChangeListener(ContainerStore.PROGRESS, this.update);
},
componentWillUnmount: function () {
var containerName = this.getParams().name;
ContainerStore.removeChangeListener(containerName, this.update);
ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.update);
ContainerStore.removeChangeListener(ContainerStore.PROGRESS, this.update);
},
update: function () {
var containerName = this.getParams().name;
var name = this.getParams().name;
var container = ContainerStore.container(name);
var progress = ContainerStore.progress(name);
this.setState({
container: ContainerStore.containers()[containerName]
progress: progress,
container: container
});
},
_escapeHTML: function (html) {
@ -137,6 +100,10 @@ var ContainerDetails = React.createClass({
return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>;
});
if (!this.state.container) {
return false;
}
var state;
if (this.state.container.State.Running) {
state = <h2 className="status">running</h2>;
@ -148,6 +115,7 @@ var ContainerDetails = React.createClass({
<div className="details">
<div className="details-header">
<h1>{this.state.container.Name.replace('/', '')}</h1>
<h2>{this.state.progress}</h2>
</div>
<div className="details-logs">
<div className="logs">

View File

@ -1,3 +1,6 @@
var async = require('async');
var _ = require('underscore');
var $ = require('jquery');
var React = require('react/addons');
var Router = require('react-router');
var Modal = require('react-bootstrap/Modal');
@ -6,10 +9,7 @@ var ModalTrigger = require('react-bootstrap/ModalTrigger');
var ContainerModal = require('./ContainerModal.react');
var ContainerStore = require('./ContainerStore');
var Header = require('./Header.react');
var async = require('async');
var _ = require('underscore');
var docker = require('./docker');
var $ = require('jquery');
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
@ -24,37 +24,34 @@ var ContainerList = React.createClass({
};
},
componentDidMount: function () {
this.update();
if (this.state.containers.length > 0) {
var name = this.state.containers[0].Name.replace('/', '');
active = name;
ContainerStore.setActive(name);
}
ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.update);
ContainerStore.addChangeListener(ContainerStore.ACTIVE, this.update);
this.updateContainers();
ContainerStore.addChangeListener(ContainerStore.ACTIVE, this.updateActive);
ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.updateContainers);
},
componentWillMount: function () {
this._start = Date.now();
},
componentWillUnmount: function () {
ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.update);
ContainerStore.removeChangeListener(ContainerStore.ACTIVE, this.update);
ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.updateContainers);
ContainerStore.removeChangeListener(ContainerStore.ACTIVE, updateActive.update);
},
componentDidUpdate: function () {
updateActive: function () {
if (ContainerStore.active()) {
this.transitionTo('container', {name: ContainerStore.active()});
}
},
update: function () {
updateContainers: function () {
// Sort by name
var containers = _.values(ContainerStore.containers()).sort(function (a, b) {
return a.Name.localeCompare(b.Name);
});
this.setState({
active: ContainerStore.active(),
containers: containers
});
this.setState({containers: containers});
if (ContainerStore.active()) {
this.transitionTo('container', {name: ContainerStore.active()});
// Transition to the active container or set one
var active = ContainerStore.active();
if (!ContainerStore.container(active) && containers.length > 0) {
ContainerStore.setActive(containers[0].Name.replace('/', ''));
}
},
handleClick: function (containerId) {
@ -77,13 +74,12 @@ var ContainerList = React.createClass({
var imageName = downloadingImage || container.Config.Image;
var state;
// Synchronize all animations
var style = {
WebkitAnimationDelay: (self._start - Date.now()) + 'ms'
};
var state;
if (downloading) {
state = <div className="state state-downloading"><div style={style} className="downloading-arrow"></div></div>;
} else if (container.State.Running && !container.State.Paused) {
@ -123,52 +119,4 @@ var ContainerList = React.createClass({
}
});
var Containers = React.createClass({
getInitialState: function () {
return {
sidebarOffset: 0
};
},
handleScroll: function (e) {
if (e.target.scrollTop > 0 && !this.state.sidebarOffset) {
this.setState({
sidebarOffset: e.target.scrollTop
});
} else if (e.target.scrollTop === 0 && this.state.sidebarOffset) {
this.setState({
sidebarOffset: 0
});
}
},
render: function () {
var sidebarHeaderClass = 'sidebar-header';
if (this.state.sidebarOffset) {
sidebarHeaderClass += ' sep';
}
return (
<div className="containers">
<Header/>
<div className="containers-body">
<div className="sidebar">
<section className={sidebarHeaderClass}>
<h3>containers</h3>
<div className="create">
<ModalTrigger modal={<ContainerModal/>}>
<div className="wrapper">
<span className="icon icon-add-3"></span>
</div>
</ModalTrigger>
</div>
</section>
<section className="sidebar-containers" onScroll={this.handleScroll}>
<ContainerList/>
</section>
</div>
<RouteHandler/>
</div>
</div>
);
}
});
module.exports = Containers;
module.exports = ContainerList;

View File

@ -8,6 +8,7 @@ var ContainerStore = require('./ContainerStore');
var Navigation = Router.Navigation;
var ContainerModal = React.createClass({
_searchRequest: null,
getInitialState: function () {
return {
query: '',
@ -19,9 +20,11 @@ var ContainerModal = React.createClass({
},
search: function (query) {
var self = this;
$.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) {
self.setState(result);
console.log(result);
this._searchRequest = $.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) {
if (self.isMounted()) {
self.setState(result);
console.log(result);
}
});
},
handleChange: function (e) {
@ -30,6 +33,11 @@ var ContainerModal = React.createClass({
if (query === this.state.query) {
return;
}
if (this._searchRequest) {
this._searchRequest.abort();
this._searchRequest = null;
}
clearTimeout(this.timeout);
var self = this;
this.timeout = setTimeout(function () {

View File

@ -1,89 +1,21 @@
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var assign = require('react/lib/Object.assign');
var docker = require('./docker.js');
var docker = require('./docker');
var registry = require('./registry');
var $ = require('jquery');
var _ = require('underscore');
// Merge our store with Node's Event Emitter
var ContainerStore = assign(EventEmitter.prototype, {
CONTAINERS: 'containers',
PROGRESS: 'progress',
LOGS: 'logs',
ACTIVE: 'active',
_containers: {},
_progress: {},
_logs: {},
_active: null,
init: function (callback) {
// TODO: Load cached data from db on loading
// Refresh with docker & hook into events
var self = this;
this.update(function (err) {
callback();
var downloading = _.filter(_.values(self._containers), function (container) {
var env = container.Config.Env;
return _.indexOf(env, 'KITEMATIC_DOWNLOADING=true') !== -1;
});
// Recover any pulls that were happening
downloading.forEach(function (container) {
var env = _.object(container.Config.Env.map(function (e) {
return e.split('=');
}));
docker.client().pull(env.KITEMATIC_DOWNLOADING_IMAGE, function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (data) {
console.log(data);
});
stream.on('end', function () {
self._createContainer(env.KITEMATIC_DOWNLOADING_IMAGE, container.Name.replace('/', ''), function () {
console.log('RECOVERED');
});
});
});
});
docker.client().getEvents(function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (data) {
console.log(data);
// TODO: Dont refresh on deleting placeholder containers
self.update(function (err) {
console.log('Updated container data.');
});
});
});
});
},
update: function (callback) {
var self = this;
docker.client().listContainers({all: true}, function (err, containers) {
if (err) {
callback(err);
return;
}
async.map(containers, function(container, callback) {
docker.client().getContainer(container.Id).inspect(function (err, data) {
callback(err, data);
});
}, function (err, results) {
if (err) {
callback(err);
return;
}
var containers = {};
results.forEach(function (r) {
containers[r.Name.replace('/', '')] = r;
});
self._containers = containers;
_.keys(self._containers).forEach(function(c) {
self.emit(c);
});
self.emit(self.CONTAINERS);
callback(null);
});
});
},
_pullScratchImage: function (callback) {
var image = docker.client().getImage('scratch:latest');
image.inspect(function (err, data) {
@ -163,41 +95,156 @@ var ContainerStore = assign(EventEmitter.prototype, {
}
}
},
// Returns all containers
containers: function() {
return this._containers;
init: function (callback) {
// TODO: Load cached data from db on loading
// Refresh with docker & hook into events
var self = this;
this.update(function (err) {
callback();
var downloading = _.filter(_.values(self._containers), function (container) {
var env = container.Config.Env;
return _.indexOf(env, 'KITEMATIC_DOWNLOADING=true') !== -1;
});
// Recover any pulls that were happening
downloading.forEach(function (container) {
var env = _.object(container.Config.Env.map(function (e) {
return e.split('=');
}));
docker.client().pull(env.KITEMATIC_DOWNLOADING_IMAGE, function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (data) {
console.log(data);
});
stream.on('end', function () {
self._createContainer(env.KITEMATIC_DOWNLOADING_IMAGE, container.Name.replace('/', ''), function () {
});
});
});
});
docker.client().getEvents(function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (data) {
console.log(data);
// TODO: Dont refresh on deleting placeholder containers
var deletingPlaceholder = data.status === 'destroy' && self.container(data.id) && self.container(data.id).Config.Env.indexOf('KITEMATIC_DOWNLOADING=true') !== -1;
console.log(deletingPlaceholder);
if (!deletingPlaceholder) {
self.update(function (err) {
console.log('Updated container data.');
});
}
});
});
});
},
update: function (callback) {
var self = this;
docker.client().listContainers({all: true}, function (err, containers) {
if (err) {
callback(err);
return;
}
async.map(containers, function(container, callback) {
docker.client().getContainer(container.Id).inspect(function (err, data) {
callback(err, data);
});
}, function (err, results) {
if (err) {
callback(err);
return;
}
var containers = {};
results.forEach(function (r) {
containers[r.Name.replace('/', '')] = r;
});
self._containers = containers;
self.emit(self.CONTAINERS);
callback(null);
});
});
},
create: function (repository, tag, callback) {
var containerName = this._generateName(repository);
tag = tag || 'latest';
var imageName = repository + ':' + tag;
// Check if image is not local or already being downloaded
console.log('Creating container.');
var self = this;
var imageName = repository + ':' + tag;
var containerName = this._generateName(repository);
var image = docker.client().getImage(imageName);
console.log(image);
image.inspect(function (err, data) {
// TODO: Get image size from registry API
/*$.get('https://registry.hub.docker.com/v1/repositories/' + repository + '/tags/' + tag, function (data) {
});*/
image.inspect(function (err, data) {
if (!data) {
// Pull image
self._createPlaceholderContainer(imageName, containerName, function (err, container) {
if (err) {
console.log(err);
}
console.log('Placeholder container created.');
docker.client().pull(imageName, function (err, stream) {
console.log(containerName);
callback(null, containerName);
stream.setEncoding('utf8');
stream.on('data', function (data) {
// TODO: update progress
//console.log(data);
});
stream.on('end', function () {
self._createContainer(imageName, containerName, function () {
registry.layers(repository, tag, function (err, layerSizes) {
if (err) {
callback(err);
}
// 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}, function(err, 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(imageName, function (err, stream) {
callback(null, containerName);
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;
}, {});
self._progress[containerName] = 0;
self.emit(containerName);
stream.on('data', function (str) {
console.log(str);
var data = JSON.parse(str);
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;
});
var totalProgress = totalReceived / totalBytes;
self._progress[containerName] = totalProgress;
self.emit(self.PROGRESS);
});
stream.on('end', function () {
self._createContainer(imageName, containerName, function () {
delete self._progress[containerName];
});
});
});
});
});
@ -210,19 +257,27 @@ var ContainerStore = assign(EventEmitter.prototype, {
});
}
});
// If so then create a container w/ kitematic-only 'downloading state'
// Pull image
// When image is done pulling then
},
setActive: function (containerName) {
this._active = containerName;
this.emit(self.ACTIVE);
setActive: function (name) {
console.log('set active');
this._active = name;
this.emit(this.ACTIVE);
},
active: function () {
return this._active;
},
logs: function (containerName) {
return logs[containerId];
// Returns all containers
containers: function() {
return this._containers;
},
container: function (name) {
return this._containers[name];
},
progress: function (name) {
return this._progress[name];
},
logs: function (name) {
return logs[name];
},
addChangeListener: function(eventType, callback) {
this.on(eventType, callback);

View File

@ -8,8 +8,8 @@ var boot2docker = require('./boot2docker.js');
var Setup = require('./Setup.react');
var Containers = require('./Containers.react');
var ContainerDetails = require('./ContainerDetails.react');
var ContainerStore = require('./ContainerStore.js');
var Radial = require('./Radial.react.js');
var ContainerStore = require('./ContainerStore');
var Radial = require('./Radial.react');
var Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute;
@ -17,6 +17,14 @@ var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
var App = React.createClass({
render: function () {
return (
<RouteHandler/>
);
}
});
var NoContainers = React.createClass({
render: function () {
return (
@ -27,14 +35,6 @@ var NoContainers = React.createClass({
}
});
var App = React.createClass({
render: function () {
return (
<RouteHandler/>
);
}
});
var routes = (
<Route name="app" path="/" handler={App}>
<Route name="containers" handler={Containers}>

84
app/registry.js Normal file
View File

@ -0,0 +1,84 @@
var async = require('async');
var $ = require('jquery');
var Registry = {
token: function(repository, callback) {
$.ajax({
url: 'https://registry.hub.docker.com/v1/repositories/' + repository + '/images',
headers: {
'X-Docker-Token': true,
},
success: function (res, status, xhr) {
callback(null, xhr.getResponseHeader('X-Docker-Token'));
},
error: function (err) {
callback(err);
}
});
},
ancestry: function (imageId, token, callback) {
$.ajax({
url: 'https://registry-1.docker.io/v1/images/' + imageId + '/ancestry',
headers: {
Authorization: 'Token ' + token
},
success: function (layers, status, xhr) {
callback(null, layers);
},
error: function (err) {
callback(err);
}
});
},
imageId: function (repository, tag, token, callback) {
$.ajax({
url: 'https://registry-1.docker.io/v1/repositories/' + repository + '/tags/' + tag,
headers: {
Authorization: 'Token ' + token
},
success: function (res, status, xhr) {
callback(null, res);
},
error: function (err) {
callback(err);
}
});
},
// Returns an array [{Id: <12 character image ID, size: size of layer in bytes}]
layers: function (repository, tag, callback) {
var self = this;
this.token(repository, function (err, token) {
self.imageId(repository, tag, token, function (err, imageId) {
self.ancestry(imageId, token, function (err, layers) {
async.map(layers, function (layer, callback) {
$.ajax({
url: 'https://registry-1.docker.io/v1/images/' + layer + '/json',
headers: {
Authorization: 'Token ' + token
},
success: function (res, status, xhr) {
var size = xhr.getResponseHeader('X-Docker-Size');
callback(null, {
Id: layer.slice(0, 12),
size: parseInt(size, 10)
});
},
error: function (err) {
callback(err);
}
});
}, function (err, results) {
if (err) {
callback('Could not sum' + err);
return;
}
callback(null, results);
});
});
});
});
}
};
module.exports = Registry;