Pull & run a container from DockerHub
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 276 B |
|
After Width: | Height: | Size: 555 B |
|
After Width: | Height: | Size: 619 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 584 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 726 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -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>
|
||||
|
|
|
|||
15
app/main.js
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||