From 183517b054ceaea933ba95dca525cd8d127da1e7 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Tue, 20 Jan 2015 21:49:14 -0500 Subject: [PATCH] Image sizes & total progress. Restructure events --- app/ContainerDetails.react.js | 62 +---- ...ainers.react.js => ContainerList.react.js} | 94 ++----- app/ContainerModal.react.js | 14 +- app/ContainerStore.js | 263 +++++++++++------- app/main.js | 20 +- app/registry.js | 84 ++++++ 6 files changed, 300 insertions(+), 237 deletions(-) rename app/{Containers.react.js => ContainerList.react.js} (66%) create mode 100644 app/registry.js diff --git a/app/ContainerDetails.react.js b/app/ContainerDetails.react.js index 0e1d5d9481..d867cc9746 100644 --- a/app/ContainerDetails.react.js +++ b/app/ContainerDetails.react.js @@ -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

; }); + if (!this.state.container) { + return false; + } + var state; if (this.state.container.State.Running) { state =

running

; @@ -148,6 +115,7 @@ var ContainerDetails = React.createClass({

{this.state.container.Name.replace('/', '')}

+

{this.state.progress}

diff --git a/app/Containers.react.js b/app/ContainerList.react.js similarity index 66% rename from app/Containers.react.js rename to app/ContainerList.react.js index c87968ad5c..f63d07b534 100644 --- a/app/Containers.react.js +++ b/app/ContainerList.react.js @@ -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 =
; } 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 ( -
-
-
-
-
-

containers

-
- }> -
- -
-
-
-
-
- -
-
- -
-
- ); - } -}); - -module.exports = Containers; +module.exports = ContainerList; diff --git a/app/ContainerModal.react.js b/app/ContainerModal.react.js index 8b5d74cdb0..a01de96227 100644 --- a/app/ContainerModal.react.js +++ b/app/ContainerModal.react.js @@ -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 () { diff --git a/app/ContainerStore.js b/app/ContainerStore.js index cb401dd744..2f75319cf2 100644 --- a/app/ContainerStore.js +++ b/app/ContainerStore.js @@ -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); diff --git a/app/main.js b/app/main.js index 05e9e31515..aa765b88f4 100644 --- a/app/main.js +++ b/app/main.js @@ -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 ( + + ); + } +}); + var NoContainers = React.createClass({ render: function () { return ( @@ -27,14 +35,6 @@ var NoContainers = React.createClass({ } }); -var App = React.createClass({ - render: function () { - return ( - - ); - } -}); - var routes = ( diff --git a/app/registry.js b/app/registry.js new file mode 100644 index 0000000000..2fc0dcf0be --- /dev/null +++ b/app/registry.js @@ -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;