Pull & run a container from DockerHub

This commit is contained in:
Jeffrey Morgan 2015-01-18 21:58:46 -05:00
parent 687264c23c
commit 1233eec3dd
19 changed files with 279 additions and 84 deletions

View File

@ -13,25 +13,12 @@ var docker = require('./docker.js');
var ContainerDetails = React.createClass({
mixins: [Router.State],
componentDidMount: function () {
ContainerStore.addChangeListener(this.update);
getInitialState: function () {
return {
logs: []
};
},
componentWillUnmount: function () {
ContainerStore.removeChangeListener(this.update);
},
update: function () {
var containerId = this.getParams().Id;
this.setState({
container: ContainerStore.containers()[containerId]
});
},
_escapeHTML: function (html) {
var text = document.createTextNode(html);
var div = document.createElement('div');
div.appendChild(text);
return div.innerHTML;
},
componentWillReceiveProps: function () {
componentWillMount: function () {
this.update();
var self = this;
var logs = [];
@ -74,15 +61,27 @@ var ContainerDetails = React.createClass({
});
});
},
componentDidMount: function () {
ContainerStore.addChangeListener(this.update);
},
componentWillUnmount: function () {
ContainerStore.removeChangeListener(this.update);
},
update: function () {
var containerId = this.getParams().Id;
this.setState({
container: ContainerStore.containers()[containerId]
});
},
_escapeHTML: function (html) {
var text = document.createTextNode(html);
var div = document.createElement('div');
div.appendChild(text);
return div.innerHTML;
},
render: function () {
var self = this;
if (!this.state || !this.state.logs) {
return false;
}
// console.log(container);
if (!this.state) {
return <div></div>;
}

View File

@ -3,22 +3,49 @@ var async = require('async');
var assign = require('react/lib/Object.assign');
var docker = require('./docker.js');
var $ = require('jquery');
var _ = require('underscore');
// Merge our store with Node's Event Emitter
var ContainerStore = assign(EventEmitter.prototype, {
_containers: {},
init: function () {
// TODO: Load cached data from leveldb
// Check if the pulled image is working
_logs: {},
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) {
self.update(function (err) {
// TODO: Make
self.update(function (err) {
console.log('Updated container data.');
});
});
});
@ -45,57 +72,133 @@ var ContainerStore = assign(EventEmitter.prototype, {
containers[r.Id] = r;
});
self._containers = containers;
console.log(containers);
self.emit('change');
callback(null);
});
});
},
_createContainer: function (image) {
docker.client().createContainer({
Image: image,
Tty: false
}, function (err, container) {
_pullScratchImage: function (callback) {
var image = docker.client().getImage('scratch:latest');
image.inspect(function (err, data) {
if (err) {
callback(err, null);
callback(err);
return;
}
console.log('Created container: ' + container.id);
container.start({
PublishAllPorts: true
}, function (err) {
if (err) { callback(err, null); return; }
console.log('Started container: ' + container.id);
callback(null, container);
if (!data) {
docker.client().pull('scratch:latest', function (err, stream) {
if (err) {
callback(err);
return;
}
stream.setEncoding('utf8');
stream.on('data', function (data) {});
stream.on('end', function () {
callback();
});
});
} else {
callback();
}
});
},
_createContainer: function (image, name, callback) {
var existing = docker.client().getContainer(name);
existing.remove(function (err, data) {
console.log('Placeholder removed.');
docker.client().createContainer({
Image: image,
Tty: false,
name: name
}, function (err, container) {
if (err) {
callback(err, null);
return;
}
console.log('Created container: ' + container.id);
container.start({
PublishAllPorts: true
}, function (err) {
if (err) { callback(err, null); return; }
console.log('Started container: ' + container.id);
callback(null, container);
});
});
});
},
_createPlaceholderContainer: function (imageName, name, callback) {
console.log('_createPlaceholderContainer', imageName, name);
this._pullScratchImage(function (err) {
docker.client().createContainer({
Image: 'scratch:latest',
Tty: false,
Env: [
'KITEMATIC_DOWNLOADING=true',
'KITEMATIC_DOWNLOADING_IMAGE=' + imageName
],
Cmd: 'placeholder',
name: name
}, function (err, container) {
callback(err, container);
});
});
},
_generateName: function (repository) {
var base = _.last(repository.split('/'));
var count = 1;
var name = base;
while (true) {
var exists = _.findWhere(_.values(this._containers), {Name: '/' + name}) || _.findWhere(_.values(this._containers), {Name: name});
if (!exists) {
return name;
} else {
count++;
name = base + '-' + count;
}
}
},
create: function (repository, tag, callback) {
console.log('create', repository, tag);
var containerName = this._generateName(repository);
tag = tag || 'latest';
var name = repository + ':' + tag;
var imageName = repository + ':' + tag;
// Check if image is not local or already being downloaded
console.log('Creating container.');
var self = this;
var image = docker.client().getImage(name);
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) {
});*/
if (data === null) {
if (!data) {
// Pull image
docker.client().pull(name, function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (data) {
console.log(data);
});
stream.on('end', function () {
self._createContainer(name);
self._createPlaceholderContainer(imageName, containerName, function (err, container) {
if (err) {
console.log(err);
}
console.log('Placeholder container created.');
docker.client().pull(imageName, function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (data) {
console.log(data);
});
stream.on('end', function () {
self._createContainer(imageName, containerName, function () {
});
});
});
});
// Create placeholder container
} else {
// If not then directly create the container
self._createContainer(name);
self._createContainer(imageName, containerName, function () {
console.log('done');
});
}
});
// If so then create a container w/ kitematic-only 'downloading state'

View File

@ -24,10 +24,17 @@ var ContainerList = React.createClass({
};
},
handleClick: function () {
console.log('hi');
},
componentDidMount: function () {
this.update();
ContainerStore.addChangeListener(this.update);
if (this.state.containers.length > 0) {
this.transitionTo('container', {Id: this.state.containers[0].Id});
}
},
componentWillMount: function () {
this._start = Date.now();
},
componentWillUnmount: function () {
ContainerStore.removeChangeListener(this.update);
@ -36,24 +43,50 @@ var ContainerList = React.createClass({
var containers = _.values(ContainerStore.containers()).sort(function (a, b) {
return a.Name.localeCompare(b.Name);
});
console.log(containers);
if (containers.length > 0) {
this.transitionTo('container', {Id: containers[0].Id, container: containers[0]});
}
this.setState({
containers: containers
});
},
render: function () {
var self = this;
var containers = this.state.containers.map(function (container) {
var downloadingImage = null, downloading = false;
var env = container.Config.Env;
if (env.length) {
var obj = _.object(env.map(function (e) {
return e.split('=');
}));
if (obj.KITEMATIC_DOWNLOADING) {
downloading = true;
}
downloadingImage = obj.KITEMATIC_DOWNLOADING_IMAGE || null;
}
var imageName = downloadingImage || container.Config.Image;
var state;
if (container.State.Running) {
state = <div className="state state-running"><div className="runningwave"></div></div>;
// Synchronize all animations
var style = {
WebkitAnimationDelay: (self._start - Date.now()) + 'ms'
};
if (downloading) {
state = <div className="state state-downloading"><div style={style} className="downloading-arrow"></div></div>;
} else if (container.State.Running && !container.State.Paused) {
state = <div className="state state-running"><div style={style} className="runningwave"></div></div>;
} else if (container.State.Restarting) {
state = <div className="state state-restarting" style={style}></div>;
} else if (container.State.Paused) {
state = <div className="state state-paused"></div>;
} else if (container.State.ExitCode) {
// state = <div className="state state-error"></div>;
state = <div className="state state-stopped"></div>;
} else {
state = <div className="state state-restarting"></div>;
state = <div className="state state-stopped"></div>;
}
return (
<Link key={container.Id} to="container" params={{Id: container.Id, container: container}} onClick={this.handleClick}>
<Link key={container.Id} to="container" params={{Id: container.Id}} onClick={this.handleClick}>
<li>
{state}
<div className="info">
@ -61,7 +94,7 @@ var ContainerList = React.createClass({
{container.Name.replace('/', '')}
</div>
<div className="image">
{container.Config.Image}
{imageName}
</div>
</div>
</li>
@ -94,7 +127,7 @@ var Containers = React.createClass({
}
},
handleClick: function () {
// ContainerStore.create('jbfink/wordpress', 'latest');
ContainerStore.create('dockerfile/ghost', 'latest', 'testghost');
},
render: function () {
var sidebarHeaderClass = 'sidebar-header';
@ -102,12 +135,12 @@ var Containers = React.createClass({
sidebarHeaderClass += ' sep';
}
return (
<div className="containers" onClick={this.handleClick}>
<div className="containers">
<Header/>
<div className="containers-body">
<div className="sidebar">
<section className={sidebarHeaderClass}>
<h3>containers</h3>
<h3 onClick={this.handleClick}>containers</h3>
<div className="create">
<ModalTrigger modal={<ContainerModal/>}>
<div className="wrapper">

View File

@ -9,6 +9,7 @@ var boot2docker = require('./boot2docker.js');
var virtualbox = require('./virtualbox.js');
var util = require('./util.js');
var docker = require('./docker.js');
var ContainerStore = require('./ContainerStore.js');
var setupSteps = [
{

View File

@ -156,6 +156,11 @@ var Boot2Docker = {
}
});
},
createScratchImage: function (callback) {
cmdExec([Boot2Docker.command(), 'ssh', 'tar cv --files-from /dev/null | docker import - scratch'], function (err, out) {
callback(err);
});
},
stats: function (callback) {
var self = this;
self.state(function (err, state) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

BIN
app/images/downloading.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
app/images/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

BIN
app/images/error@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
app/images/paused.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

BIN
app/images/paused@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
app/images/stopped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

BIN
app/images/stopped@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -2,6 +2,7 @@
<html>
<head>
<link rel="stylesheet" href="main.css"/>
<meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self' http://localhost:35729; style-src 'self' 'unsafe-inline';">
</head>
<body>
<script src="main.js"></script>

View File

@ -21,16 +21,13 @@ var NoContainers = React.createClass({
render: function () {
return (
<div>
<Radial spin="true" progress="92"/>
No Containers
</div>
);
}
});
var App = React.createClass({
componentWillMount: function () {
ContainerStore.init();
},
render: function () {
return (
<RouteHandler/>
@ -53,8 +50,14 @@ var routes = (
Router.run(routes, function (Handler) {
boot2docker.ip(function (err, ip) {
docker.setHost(ip);
React.render(<Handler/>, document.body);
if (!err) {
docker.setHost(ip);
ContainerStore.init(function () {
React.render(<Handler/>, document.body);
});
} else {
React.render(<Handler/>, document.body);
}
});
});

View File

@ -188,9 +188,9 @@
li {
vertical-align: middle;
margin: 11px 24px 0px;
margin: 14px 24px 0px;
border-bottom: 1px solid #efefef;
padding-bottom: 11px;
padding-bottom: 14px;
display: flex;
flex-direction: row;
@ -209,34 +209,65 @@
}
.image {
color: #999;
font-size: 12px;
font-size: 10px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.state {
margin-top: 8px;
display: inline-block;
position: relative;
min-width: 20px;
height: 20px;
}
.state-error {
.at2x('error.png', 20px, 20px);
}
.state-stopped {
.at2x('stopped.png', 20px, 20px);
}
.state-paused {
.at2x('paused.png', 20px, 20px);
}
.state-downloading {
.at2x('downloading.png', 20px, 20px);
overflow: hidden;
.downloading-arrow {
width: 20px;
height: 20px;
.at2x('downloading-arrow.png', 20px, 20px);
position: absolute;
// background-repeat: repeat;
-webkit-animation-name: translatedownload;
-webkit-animation-duration: 1.8s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: linear;
}
}
.state-running {
display: inline-block;
position: relative;
width: 20px;
height: 20px;
.at2x('running.png', 20px, 20px);
overflow: hidden;
// -webkit-mask-image: -webkit-radial-gradient(circle, white, black);
.runningwave {
position: absolute;
width: 44px;
width: 40px;
height: 20px;
left: -20px;
.at2x('runningwave.png', 20px, 20px);
// background-repeat: repeat;
-webkit-animation-name: translate;
-webkit-animation-duration: 6.0s;
-webkit-animation-name: translatewave;
-webkit-animation-duration: 8.0s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: linear;
}
@ -248,6 +279,7 @@
height: 20px;
.at2x('restarting.png', 20px, 20px);
background-repeat: repeat-x;
-webkit-animation-delay: -1s;
-webkit-animation-name: rotate;
-webkit-animation-duration: 3.0s;
-webkit-animation-iteration-count: infinite;
@ -478,7 +510,7 @@ html, body {
height: 100%;
}
@-webkit-keyframes rotate {
@-webkit-keyframes translatedownload {
from {
-webkit-transform: rotate(0deg);
}
@ -487,7 +519,7 @@ html, body {
}
}
@-webkit-keyframes translate {
@-webkit-keyframes translatewave {
from {
-webkit-transform: translateX(0px);
}
@ -495,3 +527,22 @@ html, body {
-webkit-transform: translateX(20px);
}
}
@-webkit-keyframes translatedownload {
0% {
-webkit-transform: translateY(6px);
opacity: 0;
}
25% {
opacity: 1;
-webkit-transform: translateY(6px);
}
50% {
opacity: 1;
-webkit-transform: translateY(20px);
}
100% {
opacity: 1;
-webkit-transform: translateY(20px);
}
}

View File

@ -24,7 +24,6 @@ app.on('activate-with-no-open-windows', function () {
});
app.on('ready', function() {
// Create the browser window.
var windowOptions = {
width: 1200,
height: 800,