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 () { componentWillReceiveProps: function () {
console.log('props');
this.update(); this.update();
var self = this; var self = this;
var logs = []; var logs = [];
@ -60,63 +61,25 @@ var ContainerDetails = React.createClass({
}); });
}); });
}); });
}, },
componentWillMount: function () { componentWillMount: function () {
this.update(); 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 () { componentDidMount: function () {
var containerName = this.getParams().name; ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.update);
ContainerStore.addChangeListener(containerName, this.update); ContainerStore.addChangeListener(ContainerStore.PROGRESS, this.update);
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
var containerName = this.getParams().name; ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.update);
ContainerStore.removeChangeListener(containerName, this.update); ContainerStore.removeChangeListener(ContainerStore.PROGRESS, this.update);
}, },
update: function () { update: function () {
var containerName = this.getParams().name; var name = this.getParams().name;
var container = ContainerStore.container(name);
var progress = ContainerStore.progress(name);
this.setState({ this.setState({
container: ContainerStore.containers()[containerName] progress: progress,
container: container
}); });
}, },
_escapeHTML: function (html) { _escapeHTML: function (html) {
@ -137,6 +100,10 @@ var ContainerDetails = React.createClass({
return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>; return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>;
}); });
if (!this.state.container) {
return false;
}
var state; var state;
if (this.state.container.State.Running) { if (this.state.container.State.Running) {
state = <h2 className="status">running</h2>; state = <h2 className="status">running</h2>;
@ -148,6 +115,7 @@ var ContainerDetails = React.createClass({
<div className="details"> <div className="details">
<div className="details-header"> <div className="details-header">
<h1>{this.state.container.Name.replace('/', '')}</h1> <h1>{this.state.container.Name.replace('/', '')}</h1>
<h2>{this.state.progress}</h2>
</div> </div>
<div className="details-logs"> <div className="details-logs">
<div className="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 React = require('react/addons');
var Router = require('react-router'); var Router = require('react-router');
var Modal = require('react-bootstrap/Modal'); var Modal = require('react-bootstrap/Modal');
@ -6,10 +9,7 @@ var ModalTrigger = require('react-bootstrap/ModalTrigger');
var ContainerModal = require('./ContainerModal.react'); var ContainerModal = require('./ContainerModal.react');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var Header = require('./Header.react'); var Header = require('./Header.react');
var async = require('async');
var _ = require('underscore');
var docker = require('./docker'); var docker = require('./docker');
var $ = require('jquery');
var Link = Router.Link; var Link = Router.Link;
var RouteHandler = Router.RouteHandler; var RouteHandler = Router.RouteHandler;
@ -24,37 +24,34 @@ var ContainerList = React.createClass({
}; };
}, },
componentDidMount: function () { componentDidMount: function () {
this.update(); this.updateContainers();
if (this.state.containers.length > 0) { ContainerStore.addChangeListener(ContainerStore.ACTIVE, this.updateActive);
var name = this.state.containers[0].Name.replace('/', ''); ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.updateContainers);
active = name;
ContainerStore.setActive(name);
}
ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.update);
ContainerStore.addChangeListener(ContainerStore.ACTIVE, this.update);
}, },
componentWillMount: function () { componentWillMount: function () {
this._start = Date.now(); this._start = Date.now();
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.update); ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.updateContainers);
ContainerStore.removeChangeListener(ContainerStore.ACTIVE, this.update); 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) { var containers = _.values(ContainerStore.containers()).sort(function (a, b) {
return a.Name.localeCompare(b.Name); return a.Name.localeCompare(b.Name);
}); });
this.setState({ this.setState({containers: containers});
active: ContainerStore.active(),
containers: containers
});
if (ContainerStore.active()) { // Transition to the active container or set one
this.transitionTo('container', {name: ContainerStore.active()}); var active = ContainerStore.active();
if (!ContainerStore.container(active) && containers.length > 0) {
ContainerStore.setActive(containers[0].Name.replace('/', ''));
} }
}, },
handleClick: function (containerId) { handleClick: function (containerId) {
@ -77,13 +74,12 @@ var ContainerList = React.createClass({
var imageName = downloadingImage || container.Config.Image; var imageName = downloadingImage || container.Config.Image;
var state;
// Synchronize all animations // Synchronize all animations
var style = { var style = {
WebkitAnimationDelay: (self._start - Date.now()) + 'ms' WebkitAnimationDelay: (self._start - Date.now()) + 'ms'
}; };
var state;
if (downloading) { if (downloading) {
state = <div className="state state-downloading"><div style={style} className="downloading-arrow"></div></div>; state = <div className="state state-downloading"><div style={style} className="downloading-arrow"></div></div>;
} else if (container.State.Running && !container.State.Paused) { } else if (container.State.Running && !container.State.Paused) {
@ -123,52 +119,4 @@ var ContainerList = React.createClass({
} }
}); });
var Containers = React.createClass({ module.exports = ContainerList;
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;

View File

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

View File

@ -1,89 +1,21 @@
var EventEmitter = require('events').EventEmitter; var EventEmitter = require('events').EventEmitter;
var async = require('async'); var async = require('async');
var assign = require('react/lib/Object.assign'); 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('jquery');
var _ = require('underscore'); var _ = require('underscore');
// Merge our store with Node's Event Emitter // Merge our store with Node's Event Emitter
var ContainerStore = assign(EventEmitter.prototype, { var ContainerStore = assign(EventEmitter.prototype, {
CONTAINERS: 'containers', CONTAINERS: 'containers',
PROGRESS: 'progress',
LOGS: 'logs',
ACTIVE: 'active', ACTIVE: 'active',
_containers: {}, _containers: {},
_progress: {},
_logs: {}, _logs: {},
_active: null, _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) { _pullScratchImage: function (callback) {
var image = docker.client().getImage('scratch:latest'); var image = docker.client().getImage('scratch:latest');
image.inspect(function (err, data) { image.inspect(function (err, data) {
@ -163,41 +95,156 @@ var ContainerStore = assign(EventEmitter.prototype, {
} }
} }
}, },
// Returns all containers init: function (callback) {
containers: function() { // TODO: Load cached data from db on loading
return this._containers;
// 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) { create: function (repository, tag, callback) {
var containerName = this._generateName(repository);
tag = tag || 'latest'; 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 self = this;
var imageName = repository + ':' + tag;
var containerName = this._generateName(repository);
var image = docker.client().getImage(imageName); 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) { if (!data) {
// Pull image // Pull image
self._createPlaceholderContainer(imageName, containerName, function (err, container) { self._createPlaceholderContainer(imageName, containerName, function (err, container) {
if (err) { if (err) {
console.log(err); console.log(err);
} }
console.log('Placeholder container created.'); registry.layers(repository, tag, function (err, layerSizes) {
docker.client().pull(imageName, function (err, stream) { if (err) {
console.log(containerName); callback(err);
callback(null, containerName); }
stream.setEncoding('utf8');
stream.on('data', function (data) { // TODO: Support v2 registry API
// TODO: update progress // TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
//console.log(data); // Use the per-layer pull progress % to update the total progress.
}); docker.client().listImages({all: 1}, function(err, images) {
stream.on('end', function () { var existingIds = new Set(images.map(function (image) {
self._createContainer(imageName, containerName, function () { 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) { setActive: function (name) {
this._active = containerName; console.log('set active');
this.emit(self.ACTIVE); this._active = name;
this.emit(this.ACTIVE);
}, },
active: function () { active: function () {
return this._active; return this._active;
}, },
logs: function (containerName) { // Returns all containers
return logs[containerId]; 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) { addChangeListener: function(eventType, callback) {
this.on(eventType, callback); this.on(eventType, callback);

View File

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