Merge pull request #169 from kitematic/jmorgan-wip-tests

WIP tests
This commit is contained in:
Jeffrey Morgan 2015-02-02 12:07:24 -05:00
commit b4b171d0ee
30 changed files with 667 additions and 334 deletions

View File

@ -25,6 +25,16 @@ To run the app in development:
- `npm run release`
### Unit Tests
- `npm test`
### Integration Tests
Note that integration tests need to be run a Mac and _will_ remove your existing Boot2Docker VM, containers etc.
- `npm run test:integration`
## Uninstalling
- Remove Kitematic.app

View File

@ -11,10 +11,18 @@ var BrowserWindow = require('browser-window');
var ipc = require('ipc');
var argv = require('minimist')(process.argv);
var saveVMOnQuit = false;
process.env.NODE_PATH = __dirname + '/../node_modules';
process.env.RESOURCES_PATH = __dirname + '/../resources';
process.chdir(path.join(__dirname, '..'));
if (argv.integration) {
process.env.TEST_TYPE = 'integration';
} else {
process.env.TEST_TYPE = 'test';
}
app.on('activate-with-no-open-windows', function () {
if (mainWindow) {
mainWindow.show();
@ -23,38 +31,35 @@ app.on('activate-with-no-open-windows', function () {
});
app.on('ready', function() {
var windowOptions = {
mainWindow = new BrowserWindow({
width: 1000,
height: 700,
'min-width': 1000,
'min-height': 700,
resizable: true,
frame: false
};
frame: false,
show: false
});
mainWindow = new BrowserWindow(windowOptions);
mainWindow.hide();
if (argv.test) {
mainWindow.loadUrl('file://' + __dirname + '/../specs/specs.html');
mainWindow.loadUrl('file://' + __dirname + '/../tests/tests.html');
} else {
mainWindow.loadUrl('file://' + __dirname + '/../build/index.html');
app.on('will-quit', function (e) {
if (saveVMOnQuit) {
exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {});
}
});
}
process.on('uncaughtException', app.quit);
var saveVMOnQuit = false;
app.on('will-quit', function (e) {
if (saveVMOnQuit) {
exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {});
}
});
mainWindow.webContents.on('new-window', function (e) {
e.preventDefault();
});
mainWindow.webContents.on('did-finish-load', function() {
mainWindow.show();
if (!argv.test) {
mainWindow.show();
}
mainWindow.focus();
mainWindow.setTitle('');
@ -92,12 +97,12 @@ app.on('ready', function() {
autoUpdater.quitAndInstall();
}
});
autoUpdater.checkForUpdates();
}
ipc.on('vm', function (event, arg) {
saveVMOnQuit = arg;
});
autoUpdater.checkForUpdates();
});
});

31
deps
View File

@ -1,36 +1,15 @@
#!/bin/bash
BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PLATFORM='unknown'
unamestr=`uname`
if [[ "$unamestr" == 'Linux' ]]; then
PLATFORM='linux'
elif [[ "$unamestr" == 'Darwin' ]]; then
PLATFORM='darwin'
fi
export BOOT2DOCKER_CLI_VERSION=$(node -pe "JSON.parse(process.argv[1])['boot2docker-version']" "$(cat $BASE/package.json)")
export BOOT2DOCKER_CLI_VERSION_FILE=boot2docker-$BOOT2DOCKER_CLI_VERSION
export ATOM_SHELL_VERSION=$(node -pe "JSON.parse(process.argv[1])['atom-shell-version']" "$(cat $BASE/package.json)")
export ATOM_SHELL_VERSION_FILE=atom-shell-v$ATOM_SHELL_VERSION-$PLATFORM-x64.zip
mkdir -p $BASE/cache/$PLATFORM
pushd $BASE/cache/$PLATFORM > /dev/null
if [ ! -f $ATOM_SHELL_VERSION_FILE ]; then
echo "-----> Downloading atom-shell..."
rm -rf atom-shell-*
curl -L -o $ATOM_SHELL_VERSION_FILE https://github.com/atom/atom-shell/releases/download/v$ATOM_SHELL_VERSION/atom-shell-v$ATOM_SHELL_VERSION-$PLATFORM-x64.zip
unzip $ATOM_SHELL_VERSION_FILE
fi
popd > /dev/null
BOOT2DOCKER_CLI_VERSION=$(node -pe "JSON.parse(process.argv[1])['boot2docker-version']" "$(cat $BASE/package.json)")
BOOT2DOCKER_CLI_FILE=boot2docker-$BOOT2DOCKER_CLI_VERSION
pushd $BASE/resources > /dev/null
if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then
if [ ! -f $BOOT2DOCKER_CLI_FILE ]; then
echo "-----> Downloading Boot2docker CLI..."
rm -rf boot2docker-*
curl -L -o $BOOT2DOCKER_CLI_VERSION_FILE https://github.com/boot2docker/boot2docker-cli/releases/download/v${BOOT2DOCKER_CLI_VERSION}/boot2docker-v${BOOT2DOCKER_CLI_VERSION}-darwin-amd64
chmod +x $BOOT2DOCKER_CLI_VERSION_FILE
curl -L -o $BOOT2DOCKER_CLI_FILE https://github.com/boot2docker/boot2docker-cli/releases/download/v${BOOT2DOCKER_CLI_VERSION}/boot2docker-v${BOOT2DOCKER_CLI_VERSION}-darwin-amd64
chmod +x $BOOT2DOCKER_CLI_FILE
fi
popd > /dev/null

170
gulpfile.js Normal file
View File

@ -0,0 +1,170 @@
var concat = require('gulp-concat');
var cssmin = require('gulp-cssmin');
var downloadatomshell = require('gulp-download-atom-shell');
var fs = require('fs');
var gulp = require('gulp');
var gulpif = require('gulp-if');
var gutil = require('gulp-util');
var http = require('http');
var less = require('gulp-less');
var livereload = require('gulp-livereload');
var plumber = require('gulp-plumber');
var react = require('gulp-react');
var runSequence = require('run-sequence');
var shell = require('gulp-shell');
var sourcemaps = require('gulp-sourcemaps');
var packagejson = require('./package.json');
var dependencies = Object.keys(packagejson.dependencies);
var devDependencies = Object.keys(packagejson.devDependencies);
var options = {
dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1,
test: process.argv.indexOf('test') !== -1,
integration: process.argv.indexOf('--integration') !== -1,
filename: 'Kitematic.app',
name: 'Kitematic'
};
gulp.task('js', function () {
gulp.src('src/**/*.js')
.pipe(plumber(function(error) {
gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message));
// emit the end event, to properly end the task
this.emit('end');
}))
.pipe(react())
.pipe(gulp.dest((options.dev || options.test) ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(options.dev, livereload()));
});
gulp.task('images', function() {
return gulp.src('images/*')
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(options.dev, livereload()));
});
gulp.task('styles', function () {
return gulp.src('styles/main.less')
.pipe(plumber(function(error) {
gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message));
// emit the end event, to properly end the task
this.emit('end');
}))
.pipe(gulpif(options.dev, sourcemaps.init()))
.pipe(less())
.pipe(gulpif(options.dev, sourcemaps.write()))
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(!options.dev, cssmin()))
.pipe(concat('main.css'))
.pipe(gulpif(options.dev, livereload()));
});
gulp.task('download', function (cb) {
downloadatomshell({
version: packagejson['atom-shell-version'],
outputDir: 'cache'
}, cb);
});
gulp.task('copy', function () {
gulp.src('index.html')
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(options.dev, livereload()));
gulp.src('fonts/**')
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(options.dev, livereload()));
});
gulp.task('dist', function (cb) {
var stream = gulp.src('').pipe(shell([
'rm -Rf ./dist',
'mkdir -p ./dist/osx',
'cp -R ./cache/Atom.app ./dist/osx/<%= filename %>',
'mv ./dist/osx/<%= filename %>/Contents/MacOS/Atom ./dist/osx/<%= filename %>/Contents/MacOS/<%= name %>',
'mkdir -p ./dist/osx/<%= filename %>/Contents/Resources/app',
'cp -R browser dist/osx/<%= filename %>/Contents/Resources/app',
'cp package.json dist/osx/<%= filename %>/Contents/Resources/app/',
'mkdir -p dist/osx/<%= filename %>/Contents/Resources/app/resources',
'cp -v resources/* dist/osx/<%= filename %>/Contents/Resources/app/resources/ || :',
'cp kitematic.icns dist/osx/<%= filename %>/Contents/Resources/atom.icns',
'/usr/libexec/PlistBuddy -c "Set :CFBundleVersion <%= version %>" dist/osx/<%= filename %>/Contents/Info.plist',
'/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist',
'/usr/libexec/PlistBuddy -c "Set :CFBundleName <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist',
'/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier <%= bundle %>" dist/osx/<%= filename %>/Contents/Info.plist',
'/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist'
], {
templateData: {
filename: options.filename,
name: options.name,
version: packagejson.version,
bundle: 'com.kitematic.app'
}
}));
dependencies.forEach(function (d) {
stream = stream.pipe(shell([
'cp -R node_modules/' + d + ' dist/osx/<%= filename %>/Contents/Resources/app/node_modules/'
], {
templateData: {
filename: options.filename
}
}));
});
return stream;
});
gulp.task('sign', function () {
try {
var signing_identity = fs.readFileSync('./identity', 'utf8').trim();
return gulp.src('').pipe(shell([
'codesign --deep --force --verbose --sign "' + signing_identity + '" ' + options.filename
], {
cwd: './dist/osx/'
}));
} catch (error) {
gutil.log(gutil.colors.red('Error: ' + error.message));
}
});
gulp.task('zip', function () {
return gulp.src('').pipe(shell([
'ditto -c -k --sequesterRsrc --keepParent ' + options.filename + ' ' + options.name + '-' + packagejson.version + '.zip'
], {
cwd: './dist/osx/'
}));
});
gulp.task('release', function () {
runSequence('download', 'dist', ['copy', 'images', 'js', 'styles'], 'sign', 'zip');
});
gulp.task('test', ['download', 'copy', 'js'], function () {
var env = process.env;
env.NODE_ENV = 'test';
if (options.integration) {
gulp.src('').pipe(shell(['./cache/Atom.app/Contents/MacOS/Atom . --test --integration'], {
env: env
}));
} else {
gulp.src('').pipe(shell(['./cache/Atom.app/Contents/MacOS/Atom . --test'], {
env: env
}));
}
});
gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () {
gulp.watch('src/**/*.js', ['js']);
gulp.watch('index.html', ['copy']);
gulp.watch('styles/**/*.less', ['styles']);
gulp.watch('images/**', ['images']);
livereload.listen();
var env = process.env;
env.NODE_ENV = 'development';
gulp.src('').pipe(shell(['./cache/Atom.app/Contents/MacOS/Atom .'], {
env: env
}));
});

View File

@ -11,13 +11,12 @@
},
"bugs": "https://github.com/kitematic/kitematic/issues",
"scripts": {
"start": "rsync ./index.html ./build/ && rsync ./fonts/* ./build/ && rsync ./images/* ./build && jsx --watch src/ build/ & wess -w -m -i ./styles/main.less -o ./build/main.css & wiper -w ./build/**/*.* & npm run start:$(uname) && kill $(pgrep node)",
"start:Darwin": "NODE_ENV=development ./cache/darwin/Atom.app/Contents/MacOS/Atom .",
"start:Linux": "NODE_ENV=development ./cache/linux/atom .",
"test": "jsx --watch src build/ & rsync ./images/* ./build && wess -m -i ./styles/main.less -o ./build/main.css && npm run test:$(uname) && kill $(pgrep node)",
"test:Darwin": "NODE_ENV=test ./cache/darwin/Atom.app/Contents/MacOS/Atom . --test",
"test:Linux": "NODE_ENV=test ./cache/darwin/atom .",
"release": "./release"
"start": "gulp",
"test": "gulp test --silent",
"test:integration": "gulp test --silent --integration",
"all-tests": "npm test && npm run integration-tests",
"release": "gulp run release",
"preinstall": "./deps"
},
"licenses": [
{
@ -32,15 +31,17 @@
"debug"
]
},
"boot2docker-version": "1.3.2",
"boot2docker-version": "1.4.1",
"atom-shell-version": "0.21.0",
"virtualbox-version": "4.3.20",
"virtualbox-filename": "VirtualBox-4.3.20-96996-OSX.dmg",
"virtualbox-required-version": "4.3.18",
"dependencies": {
"ansi-to-html": "0.2.0",
"async": "^0.9.0",
"bugsnag-js": "^2.4.6",
"bugsnag-js": "git+https://git@github.com/bugsnag/bugsnag-js",
"dockerode": "2.0.4",
"exec": "0.1.2",
"gulp-react": "^2.0.0",
"jest-cli": "^0.2.1",
"jquery": "^2.1.3",
"minimist": "^1.1.0",
@ -57,15 +58,33 @@
"underscore": "^1.7.0"
},
"devDependencies": {
"browserify": "^6.2.0",
"ecstatic": "^0.5.8",
"glob": "^4.0.6",
"jasmine-node": "^1.14.5",
"jasmine-reporters": "^1.0.1",
"jasmine-tagged": "^1.1.2",
"jasmine-terminal-reporter": "^0.9.1",
"react-tools": "^0.12.2",
"wess": "^0.2.2",
"wiper": "^1.0.2",
"gulp": "^3.8.10",
"gulp-atom": "0.0.5",
"gulp-concat": "^2.3.4",
"gulp-cssmin": "^0.1.6",
"gulp-download-atom-shell": "0.0.4",
"gulp-if": "^1.2.4",
"gulp-imagemin": "^2.0.0",
"gulp-less": "^2.0.1",
"gulp-livereload": "^2.1.1",
"gulp-notify": "^1.4.2",
"gulp-plumber": "^0.6.6",
"gulp-react": "^2.0.0",
"gulp-shell": "^0.2.11",
"gulp-sourcemaps": "^1.2.8",
"gulp-streamify": "0.0.5",
"gulp-uglify": "^0.3.1",
"gulp-uglifyjs": "^0.5.0",
"gulp-util": "^3.0.0",
"reactify": "^0.15.2",
"rimraf": "^2.2.8",
"run-sequence": "^1.0.2",
"time-require": "^0.1.2",
"vinyl-source-stream": "^0.1.1",
"watchify": "^2.1.1",
"zombie": "^2.5.1"
}
}

35
release
View File

@ -1,35 +0,0 @@
#!/bin/bash
BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
export NAME=$(node -pe "JSON.parse(process.argv[1])['name']" "$(cat $BASE/package.json)")
export VERSION=$(node -pe "JSON.parse(process.argv[1])['version']" "$(cat $BASE/package.json)")
export FILENAME=$NAME.app
export BUNDLE=com.kitematic.app
rm -Rf ./dist
mkdir -p ./dist/osx
cp -R ./cache/Atom.app ./dist/osx/$FILENAME
mv ./dist/osx/$FILENAME/Contents/MacOS/Atom ./dist/osx/$FILENAME/Contents/MacOS/$NAME
mkdir -p ./dist/osx/$FILENAME/Contents/Resources/app
cp -R browser dist/osx/$FILENAME/Contents/Resources/app
cp package.json dist/osx/$FILENAME/Contents/Resources/app/
mkdir -p dist/osx/$FILENAME/Contents/Resources/app/resources
mkdir -p dist/osx/$FILENAME/Contents/Resources/app/node_modules
cp -v resources/* dist/osx/$FILENAME/Contents/Resources/app/resources/ || :
cp kitematic.icns dist/osx/$FILENAME/Contents/Resources/atom.icns
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" dist/osx/$FILENAME/Contents/Info.plist
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $NAME" dist/osx/$FILENAME/Contents/Info.plist
/usr/libexec/PlistBuddy -c "Set :CFBundleName $NAME" dist/osx/$FILENAME/Contents/Info.plist
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE" dist/osx/$FILENAME/Contents/Info.plist
/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable $NAME" dist/osx/$FILENAME/Contents/Info.plist
rsync ./index.html ./dist/osx/$FILENAME/Contents/Resources/app/build/
rsync ./fonts/* ./dist/osx/$FILENAME/Contents/Resources/app/build/
rsync ./images/* ./dist/osx/$FILENAME/Contents/Resources/app/build/
jsx src/ ./dist/osx/$FILENAME/Contents/Resources/app/build/
wess -m -i ./styles/main.less -o ./dist/osx/$FILENAME/Contents/Resources/app/build/main.css
if [ -f $BASE/identity ]; then
codesign --deep --force --verbose --sign "$(cat $BASE/identity)" ./dist/osx/$FILENAME
fi
ditto -c -k --sequesterRsrc --keepParent ./dist/osx/$FILENAME ./dist/osx/$NAME-$VERSION.zip

BIN
resources/cocoasudo Executable file

Binary file not shown.

View File

@ -1,8 +0,0 @@
var ContainerStore = require('../build/ContainerStore');
var TestUtils = require('react/addons').TestUtils;
describe('ContainerStore', function() {
it('returns an empty array initially', function() {
expect(ContainerStore.containers()).toEqual({});
});
});

View File

@ -84,7 +84,13 @@ var Boot2Docker = {
cmdExec([Boot2Docker.command(), 'upgrade'], callback);
},
ip: function (callback) {
cmdExec([Boot2Docker.command(), 'ip'], callback);
exec([Boot2Docker.command(), 'ip'], function (stderr, stdout, code) {
if (code) {
callback(stderr);
} else {
callback(null, stdout.trim().replace('\n', ''));
}
});
},
erase: function (callback) {
var VMFileLocation = path.join(homeDir(), 'VirtualBox\\ VMs/boot2docker-vm');

View File

@ -76,7 +76,7 @@ var ContainerDetails = React.createClass({
var $viewPopover = $(this.getDOMNode()).find('.popover-view');
var $volumePopover = $(this.getDOMNode()).find('.popover-volume');
if ($viewDropdown && $volumeDropdown && $viewPopover && $volumePopover) {
if ($viewDropdown.offset() && $volumeDropdown.offset()) {
$viewPopover.offset({
top: $viewDropdown.offset().top + 32,
left: $viewDropdown.offset().left - ($viewPopover.outerWidth() / 2) + 14
@ -94,16 +94,19 @@ var ContainerDetails = React.createClass({
return;
}
this.setState({
progress: ContainerStore.progress(this.getParams().name),
env: ContainerUtil.env(container),
});
var ports = ContainerUtil.ports(container);
var webPorts = ['80', '8000', '8080', '3000', '5000', '2368'];
console.log(ports);
this.setState({
ports: ports,
defaultPort: _.find(_.keys(ports), function (port) {
return webPorts.indexOf(port) !== -1;
})
});
console.log(this.state);
this.updateLogs();
},
updateLogs: function (name) {
@ -134,6 +137,7 @@ var ContainerDetails = React.createClass({
handleView: function () {
if (this.state.defaultPort) {
console.log(this.state.defaultPort);
console.log(this.state.ports[this.state.defaultPort].url);
exec(['open', this.state.ports[this.state.defaultPort].url], function (err) {
if (err) { throw err; }
});
@ -159,11 +163,6 @@ var ContainerDetails = React.createClass({
console.log(err);
});
},
handleRestart: function () {
ContainerStore.restart(this.props.container.Name, function (err) {
console.log(err);
});
},
handleTerminal: function () {
var container = this.props.container;
var terminal = path.join(process.cwd(), 'resources', 'terminal').replace(/ /g, '\\\\ ');
@ -310,6 +309,13 @@ var ContainerDetails = React.createClass({
disabled: !this.props.container.State.Running
});
var restartButtonClass = React.addons.classSet({
btn: true,
'btn-action': true,
'with-icon': true,
disabled: this.props.container.State.Restarting
});
var viewButtonClass = React.addons.classSet({
btn: true,
'btn-action': true,
@ -444,7 +450,7 @@ var ContainerDetails = React.createClass({
<a className={dropdownVolumeButtonClass} onClick={this.handleVolumeDropdown}><span className="icon icon-folder-1"></span> <span className="content">Volumes</span> <span className="icon-dropdown icon icon-arrow-37"></span></a>
</div>
<div className="action">
<a className={buttonClass} onClick={this.handleRestart}><span className="icon icon-refresh"></span> <span className="content">Restart</span></a>
<a className={restartButtonClass} onClick={this.handleRestart}><span className="icon icon-refresh"></span> <span className="content">Restart</span></a>
</div>
<div className="action">
<a className={buttonClass} onClick={this.handleTerminal}><span className="icon icon-window-code-3"></span> <span className="content">Terminal</span></a>

View File

@ -83,6 +83,9 @@ var ContainerModal = React.createClass({
},
handleClick: function (name, event) {
ContainerStore.create(name, 'latest', function (err, containerName) {
if (err) {
throw err;
}
this.props.onRequestHide();
}.bind(this));
},

View File

@ -18,7 +18,6 @@ var _progress = {};
var _logs = {};
var _streams = {};
var _muted = {};
var _config = {};
var ContainerStore = assign(EventEmitter.prototype, {
CLIENT_CONTAINER_EVENT: 'client_container',
@ -250,7 +249,12 @@ var ContainerStore = assign(EventEmitter.prototype, {
init: function (callback) {
// TODO: Load cached data from db on loading
this.fetchAllContainers(function (err) {
callback();
if (err) {
callback(err);
return;
} else {
callback();
}
this.emit(this.CLIENT_CONTAINER_EVENT);
this._resumePulling();
this._startListeningToEvents();
@ -366,7 +370,6 @@ var ContainerStore = assign(EventEmitter.prototype, {
});
stream.on('end', function () {
delete _streams[name];
console.log('end', name);
});
});
},
@ -381,6 +384,10 @@ var ContainerStore = assign(EventEmitter.prototype, {
if (!data) {
// Pull image
self._createPlaceholderContainer(imageName, containerName, function (err, container) {
if (err) {
callback(err);
return;
}
_containers[containerName] = container;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create');
_muted[containerName] = true;

View File

@ -15,9 +15,8 @@ var ContainerUtil = {
ports: function (container, callback) {
var res = {};
var ip = docker.host;
console.log(container);
_.each(container.NetworkSettings.Ports, function (value, key) {
var dockerPort = key;
var dockerPort = key.split('/')[0];
var localUrl = null;
var localUrlDisplay = null;
if (value && value.length) {

View File

@ -12,8 +12,7 @@ var Docker = {
return;
}
this._client = new dockerode({
protocol: 'https',
host: this.host,
host: '192.168.59.103',
port: 2376,
ca: fs.readFileSync(path.join(certDir, 'ca.pem')),
cert: fs.readFileSync(path.join(certDir, 'cert.pem')),

View File

@ -2,23 +2,22 @@ var React = require('react');
var Router = require('react-router');
var RetinaImage = require('react-retina-image');
var async = require('async');
var docker = require('./docker');
var docker = require('./Docker');
var router = require('./router');
var boot2docker = require('./boot2docker');
var ContainerStore = require('./ContainerStore');
var SetupStore = require('./ContainerStore');
var Menu = require('./Menu');
var remote = require('remote');
var app = remote.require('app');
var ipc = require('ipc');
var Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute;
var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
if (process.env.NODE_ENV === 'development') {
var script = document.createElement('script');
script.type = 'text/javascript';
@ -28,13 +27,25 @@ if (process.env.NODE_ENV === 'development') {
}
if (!window.location.hash.length || window.location.hash === '#/') {
router.run(function (Handler) {
React.render(<Handler/>, document.body);
SetupStore.run(function (err) {
boot2docker.ip(function (err, ip) {
if (err) console.log(err);
docker.setHost(ip);
router.transitionTo('containers');
ContainerStore.init(function (err) {
if (err) console.log(err);
router.run(function (Handler) {
React.render(<Handler/>, document.body);
});
});
});
});
} else {
boot2docker.ip(function (err, ip) {
if (err) console.log(err);
docker.setHost(ip);
ContainerStore.init(function () {
ContainerStore.init(function (err) {
if (err) console.log(err);
router.run(function (Handler) {
React.render(<Handler/>, document.body);
});

View File

@ -5,133 +5,9 @@ var async = require('async');
var assign = require('object-assign');
var fs = require('fs');
var path = require('path');
var boot2docker = require('./Boot2Docker');
var virtualbox = require('./Virtualbox');
var util = require('./Util');
var docker = require('./Docker');
var ContainerStore = require('./ContainerStore');
var setupSteps = [
{
run: function (callback, progressCallback) {
console.log(util.supportDir());
var installed = virtualbox.installed();
if (!installed) {
util.download('https://s3.amazonaws.com/kite-installer/' + virtualbox.INSTALLER_FILENAME, path.join(util.supportDir(), virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) {
if (err) {callback(err); return;}
virtualbox.install(function (err) {
if (!virtualbox.installed()) {
callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.');
} else {
callback(err);
}
});
}, function (progress) {
progressCallback(progress);
});
} else {
virtualbox.version(function (err, installedVersion) {
if (err) {callback(err); return;}
if (util.compareVersions(installedVersion, virtualbox.REQUIRED_VERSION) < 0) {
// Download a newer version of Virtualbox
util.downloadFile(Setup.BASE_URL + virtualbox.INSTALLER_FILENAME, path.join(util.getResourceDir(), virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) {
if (err) {callback(err); return;}
virtualbox.kill(function (err) {
if (err) {callback(err); return;}
virtualbox.install(function (err) {
if (err) {callback(err); return;}
virtualbox.version(function (err, installedVersion) {
if (err) {callback(err); return;}
if (util.compareVersions(installedVersion, virtualbox.REQUIRED_VERSION) < 0) {
callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.');
} else {
callback(err);
}
});
});
});
}, function (progress) {
progressCallback(progress);
});
} else {
callback();
}
});
}
},
message: 'Downloading VirtualBox...'
},
{
run: function (callback) {
virtualbox.deleteVM('kitematic-vm', function (err, removed) {
if (err) {
console.log(err);
}
callback();
});
},
message: 'Cleaning up existing Docker VM...'
},
// Initialize Boot2Docker if necessary.
{
run: function (callback) {
boot2docker.exists(function (err, exists) {
if (err) { callback(err); return; }
if (!exists) {
boot2docker.init(function (err) {
callback(err);
});
} else {
if (!boot2docker.sshKeyExists()) {
callback('Boot2Docker SSH key doesn\'t exist. Fix by removing the existing Boot2Docker VM and re-run the installer. This usually occurs because an old version of Boot2Docker is installed.');
} else {
boot2docker.isoVersion(function (err, version) {
if (err || util.compareVersions(version, boot2docker.version()) < 0) {
boot2docker.stop(function(err) {
boot2docker.upgrade(function (err) {
callback(err);
});
});
} else {
callback();
}
});
}
}
});
},
message: 'Setting up the Docker VM...'
},
{
run: function (callback) {
boot2docker.waitWhileStatus('saving', function (err) {
boot2docker.status(function (err, status) {
if (err) {callback(err); return;}
if (status !== 'running') {
boot2docker.start(function (err) {
callback(err);
});
} else {
callback();
}
});
});
},
message: 'Starting the Docker VM...'
},
{
run: function (callback) {
boot2docker.ip(function (err, ip) {
if (err) {callback(err); return;}
console.log('Setting host IP to: ' + ip);
docker.setHost(ip);
callback(err);
});
},
message: 'Detecting Docker VM...'
}
];
var SetupStore = require('./SetupStore');
var Setup = React.createClass({
mixins: [ Router.Navigation ],
@ -140,6 +16,14 @@ var Setup = React.createClass({
message: '',
progress: 0
};
},
componentWillMount: function () {
SetupStore.on(SetupStore.PROGRESS_EVENT, this.update);
},
componentDidMount: function () {
},
update: function () {
},
render: function () {
var radial;
@ -165,53 +49,6 @@ var Setup = React.createClass({
</div>
);
}
},
componentWillMount: function () {
this.setState({});
},
componentDidMount: function () {
var self = this;
this.setup(function (err) {
if (!err) {
boot2docker.ip(function (err, ip) {
docker.setHost(ip);
ContainerStore.init(function () {
self.transitionTo('containers');
});
});
}
});
},
setup: function (callback) {
var self = this;
var currentStep = 0;
async.eachSeries(setupSteps, function (step, callback) {
console.log('Performing step ' + currentStep);
self.setState({progress: 0});
self.setState({message: step.message});
step.run(function (err) {
if (err) {
callback(err);
} else {
currentStep += 1;
callback();
}
}, function (progress) {
self.setState({progress: progress});
});
}, function (err) {
if (err) {
// if any of the steps fail
console.log('Kitematic setup failed at step ' + currentStep);
console.log(err);
self.setState({error: err.message});
callback(err);
} else {
// Setup Finished
console.log('Setup finished.');
callback();
}
});
}
});

201
src/SetupStore.js Normal file
View File

@ -0,0 +1,201 @@
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var async = require('async');
var fs = require('fs');
var path = require('path');
var exec = require('exec');
var boot2docker = require('./Boot2Docker');
var virtualbox = require('./Virtualbox');
var setupUtil = require('./SetupUtil');
var packagejson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
var _currentStep = null;
var _error = null;
var _progress = null;
var SetupStore = assign(EventEmitter.prototype, {
PROGRESS_EVENT: 'setup_progress',
STEP_EVENT: 'setup_step',
ERROR_EVENT: 'setup_error',
downloadVirtualboxStep: {
_download: function (callback, progressCallback) {
setupUtil.virtualboxSHA256(packagejson['virtualbox-version'], packagejson['virtualbox-filename'], function (err, checksum) {
if (err) {
callback(err);
return;
}
var url = 'http://download.virtualbox.org/virtualbox/' + packagejson['virtualbox-version'] + '/' + packagejson['virtualbox-filename'];
var downloadPath = path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']);
setupUtil.download(url, downloadPath, checksum, function (err) {
callback(err);
}, function (progress) {
progressCallback(progress);
});
});
},
run: function (callback, progressCallback) {
if (virtualbox.installed()) {
virtualbox.version(function (err, version) {
if (err) {callback(err); return;}
if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) < 0) {
this._download(callback, progressCallback);
} else {
callback();
}
});
} else {
this._download(callback, progressCallback);
}
},
name: 'downloading_virtualbox',
message: 'Downloading Virtualbox',
},
installVirtualboxStep: {
_install: function (callback) {
exec(['hdiutil', 'attach', path.join(setupUtil.supportDir(), 'VirtualBox-4.3.20-96996-OSX.dmg')], function (stderr, stdout, code) {
if (code) {
callback(stderr);
return;
}
var iconPath = path.join(setupUtil.resourceDir(), 'kitematic.icns');
setupUtil.isSudo(function (err, isSudo) {
sudoCmd = isSudo ? ['sudo'] : [path.join(setupUtil.resourceDir(), 'cocoasudo'), '--icon=' + iconPath, '--prompt=Kitematic requires administrative privileges to install VirtualBox and copy itself to the Applications folder.'];
sudoCmd.push.apply(sudoCmd, ['installer', '-pkg', '/Volumes/VirtualBox/VirtualBox.pkg', '-target', '/']);
exec(sudoCmd, function (stderr, stdout, code) {
if (code) {
console.log(stderr);
console.log(stdout);
callback('Could not install virtualbox.');
} else {
exec(['hdiutil', 'detach', '/Volumes/VirtualBox'], function(stderr, stdout, code) {
if (code) {
callback(stderr);
} else {
callback();
}
});
}
});
});
});
},
run: function (callback) {
var self = this;
if (virtualbox.installed()) {
virtualbox.version(function (err, version) {
if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) < 0) {
virtualbox.kill(function (err) {
if (err) {callback(err); return;}
self._install(function(err) {
callback(err);
});
});
} else {
callback();
}
});
} else {
self._install(function(err) {
callback(err);
});
}
},
name: 'installing_virtualbox',
message: 'Installing VirtualBox',
},
cleanupKitematicStep: {
run: function (callback) {
virtualbox.vmdestroy('kitematic-vm', function (err, removed) {
if (err) {
console.log(err);
}
callback();
});
},
name: 'cleanup_kitematic',
message: 'Cleaning up existing Kitematic install...'
},
initBoot2DockerStep: {
run: function (callback) {
boot2docker.exists(function (err, exists) {
if (err) { callback(err); return; }
if (!exists) {
boot2docker.init(function (err) {
callback(err);
});
} else {
if (!boot2docker.sshKeyExists()) {
callback('Boot2Docker SSH key doesn\'t exist. Fix by removing the existing Boot2Docker VM and re-run the installer. This usually occurs because an old version of Boot2Docker is installed.');
} else {
boot2docker.isoVersion(function (err, version) {
if (err || setupUtil.compareVersions(version, boot2docker.version()) < 0) {
boot2docker.stop(function(err) {
boot2docker.upgrade(function (err) {
callback(err);
});
});
} else {
callback();
}
});
}
}
});
},
name: 'init_boot2docker',
message: 'Setting up the Docker VM...'
},
startBoot2DockerStep: {
run: function (callback) {
boot2docker.waitWhileStatus('saving', function (err) {
boot2docker.status(function (err, status) {
if (err) {callback(err); return;}
if (status !== 'running') {
boot2docker.start(function (err) {
callback(err);
});
} else {
callback();
}
});
});
},
name: 'start_boot2docker',
message: 'Starting the Docker VM...'
},
step: function () {
return _currentStep;
},
progress: function () {
return _progress;
},
run: function (callback) {
var self = this;
var steps = [this.downloadVirtualboxStep, this.installVirtualboxStep, this.cleanupKitematicStep, this.initBoot2DockerStep, this.startBoot2DockerStep];
async.eachSeries(steps, function (step, callback) {
console.log(step.name);
_currentStep = step;
_progress = null;
step.run(function (err) {
if (err) {
callback(err);
} else {
self.emit(self.STEP_EVENT);
callback();
}
}, function (progress) {
self.emit(self.PROGRESS_EVENT, progress);
_progress = progress;
});
}, function (err) {
if (err) {
self.emit(self.ERROR_EVENT, _error);
callback(err);
} else {
callback();
}
});
}
});
module.exports = SetupStore;

View File

@ -1,13 +1,13 @@
var path = require('path');
var fs = require('fs');
var nodeCrypto = require('crypto');
var request = require('request');
var progress = require('request-progress');
var path = require('path');
var crypto = require('crypto');
var fs = require('fs');
var exec = require('exec');
var Util = {
var SetupUtil = {
supportDir: function (callback) {
var dirs = ['Application\ Support', 'Kitematic'];
var dirs = ['Library', 'Application\ Support', 'Kitematic'];
var acc = process.env.HOME;
dirs.forEach(function (d) {
acc = path.join(acc, d);
@ -17,6 +17,19 @@ var Util = {
});
return acc;
},
resourceDir: function (callback) {
return process.env.RESOURCES_PATH;
},
isSudo: function (callback) {
exec(['sudo', '-n', '-u', 'root', 'true'], function (stderr, stdout, code) {
if (code) {
callback(stderr);
} else {
var isSudo = stderr.indexOf('a password is required') === -1;
callback(null, isSudo);
}
});
},
download: function (url, filename, checksum, callback, progressCallback) {
var doDownload = function () {
progress(request({
@ -37,8 +50,7 @@ var Util = {
// Compare checksum to see if it already exists first
if (fs.existsSync(filename)) {
var existingChecksum = nodeCrypto.createHash('sha256').update(fs.readFileSync(filename), 'utf8').digest('hex');
console.log(existingChecksum);
var existingChecksum = crypto.createHash('sha256').update(fs.readFileSync(filename), 'utf8').digest('hex');
if (existingChecksum !== checksum) {
fs.unlinkSync(filename);
doDownload();
@ -49,6 +61,22 @@ var Util = {
doDownload();
}
},
virtualboxSHA256: function (version, filename, callback) {
var checksumUrl = 'http://dlc-cdn.sun.com/virtualbox/' + version + '/SHA256SUMS';
request(checksumUrl, function (error, response, body) {
if (error) {
callback(error);
return;
}
var checksums = body.split('\n').map(function (line) {
return line.split(' *');
}).reduce(function (obj, pair) {
obj[pair[1]] = pair[0];
return obj;
}, {});
callback(null, checksums[filename]);
});
},
compareVersions: function (v1, v2, options) {
var lexicographical = options && options.lexicographical,
zeroExtend = options && options.zeroExtend,
@ -100,4 +128,4 @@ var Util = {
}
};
module.exports = Util;
module.exports = SetupUtil;

View File

@ -5,32 +5,18 @@ var async = require('async');
var util = require('./Util');
var VirtualBox = {
REQUIRED_VERSION: '4.3.18',
INCLUDED_VERSION: '4.3.18',
INSTALLER_FILENAME: 'virtualbox-4.3.18.pkg',
INSTALLER_CHECKSUM: '5836c94481c460c648b9216386591a2915293ac86b9bb6c57746637796af6af2',
command: function () {
return '/usr/bin/VBoxManage';
},
installed: function () {
return fs.existsSync('/usr/bin/VBoxManage') && fs.existsSync('/Applications/VirtualBox.app/Contents/MacOS/VirtualBox');
},
install: function (callback) {
// -W waits for the process to close before finishing.
exec('open -W ' + path.join(util.supportDir(), this.INSTALLER_FILENAME).replace(' ', '\\ '), function (stderr, stdout, code) {
if (code) {
callback(stderr);
return;
}
callback(null);
});
},
version: function (callback) {
if (!this.installed()) {
callback('VirtualBox not installed.');
return;
}
exec('/usr/bin/VBoxManage -v', function (stderr, stdout, code) {
exec([this.command(), '-v'], function (stderr, stdout, code) {
if (code) {
callback(stderr);
return;
@ -44,12 +30,12 @@ var VirtualBox = {
callback(null, match[1]);
});
},
saveVMs: function (callback) {
poweroff: function (callback) {
if (!this.installed()) {
callback('VirtualBox not installed.');
return;
}
exec('/usr/bin/VBoxManage list runningvms | sed -E \'s/.*\\{(.*)\\}/\\1/\' | xargs -L1 -I {} /usr/bin/VBoxManage controlvm {} savestate', function (stderr, stdout, code) {
exec(this.command() + ' list runningvms | sed -E \'s/.*\\{(.*)\\}/\\1/\' | xargs -L1 -I {} ' + this.command() + ' controlvm {} acpipowerbutton', function (stderr, stdout, code) {
if (code) {
callback(stderr);
} else {
@ -58,7 +44,7 @@ var VirtualBox = {
});
},
kill: function (callback) {
this.saveRunningVMs(function (err) {
this.poweroff(function (err) {
if (err) {callback(err); return;}
exec('pkill VirtualBox', function (stderr, stdout, code) {
if (code) {callback(stderr); return;}
@ -69,7 +55,7 @@ var VirtualBox = {
});
});
},
vmState: function (name, callback) {
vmstate: function (name, callback) {
exec(this.command() + ' showvminfo ' + name + ' --machinereadable', function (stderr, stdout, code) {
if (code) { callback(stderr); return; }
var match = stdout.match(/VMState="(\w+)"/);
@ -80,8 +66,9 @@ var VirtualBox = {
callback(null, match[1]);
});
},
deleteVM:function (name, callback) {
VirtualBox.vmState(name, function (err, state) {
vmdestroy: function (name, callback) {
var self = this;
this.vmstate(name, function (err, state) {
// No VM found
if (err) { callback(null, false); return; }
exec('/usr/bin/VBoxManage controlvm ' + name + ' acpipowerbutton', function (stderr, stdout, code) {
@ -91,7 +78,7 @@ var VirtualBox = {
async.until(function () {
return state === 'poweroff';
}, function (callback) {
VirtualBox.vmState(name, function (err, newState) {
self.vmstate(name, function (err, newState) {
if (err) { callback(err); return; }
state = newState;
setTimeout(callback, 250);

View File

@ -0,0 +1,99 @@
var virtualbox = require('../build/Virtualbox');
var SetupStore = require('../build/SetupStore');
var setupUtil = require('../build/SetupUtil');
var path = require('path');
var fs = require('fs');
var child_process = require('child_process');
var exec = require('exec');
var rimraf = require('rimraf');
var packagejson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
jasmine.DEFAULT_TIMEOUT_INTERVAL = 300000; // 5 minutes
describe('Setup', function () {
describe('without virtualbox installed or downloaded', function () {
var virtualboxFile = path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']);
beforeEach(function () {
if (fs.existsSync(virtualboxFile)) {
fs.unlinkSync(virtualboxFile);
}
spyOn(virtualbox, 'installed').and.returnValue(false);
});
it('downloads virtualbox from the official website', function (done) {
SetupStore.downloadVirtualboxStep.run(function (err) {
expect(err).toBeFalsy();
expect(fs.existsSync(virtualboxFile)).toBe(true);
done();
}, function (progress) {
});
});
});
describe('with virtualbox downloaded but not installed', function () {
beforeEach(function (done) {
// 5 minute timeout per test
SetupStore.downloadVirtualboxStep.run(function (err) {
if (virtualbox.installed()) {
virtualbox.kill(function (callback) {
done();
});
} else {
done();
}
}, function (progress) {});
});
it('does install virtualbox', function (done) {
SetupStore.installVirtualboxStep.run(function (err) {
expect(err).toBeFalsy();
expect(fs.existsSync(virtualbox.command())).toBe(true);
done();
});
});
});
describe('with virtualbox installed', function () {
// Before each teardown the boot2docker VM, keys and anything else
describe('and with a kitematic vm', function () {
});
describe('and without a boot2docker vm', function () {
});
describe('and with an old boot2docker vm', function () {
});
describe('and with a very old boot2docker vm', function () {
});
describe('and with a boot2docker vm running', function () {
});
describe('and with a boot2docker vm but with no ssh keys', function () {
});
describe('and with a boot2docker vm being powered off', function () {
});
describe('and with a boot2docker vm being removed', function () {
});
describe('and with a boot2docker vm initialized but not running', function () {
});
});
});

View File

@ -0,0 +1,10 @@
var setupUtil = require('../build/SetupUtil');
describe('SetupUtils', function() {
it('returns live sha256 checksum for a given virtualbox version & filename', function (done) {
setupUtil.virtualboxSHA256('4.3.20', 'VirtualBox-4.3.20-96996-OSX.dmg', function (err, checksum) {
expect(checksum).toBe('744e77119a640a5974160213c9912568a3d88dbd06a2fc6b6970070941732705');
done();
});
});
});

View File

@ -57,8 +57,6 @@ getJasmineRequireObj().ConsoleReporter = function() {
},
failedSuites = [];
print('ConsoleReporter is deprecated and will be removed in a future version.');
this.jasmineStarted = function() {
specCount = 0;
failureCount = 0;

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -4,7 +4,6 @@
<link rel="stylesheet" href="jasmine-2.1.3/jasmine.css"/>
</head>
<body>
<script src="specs.js"></script>
<script src="http://localhost:35729"></script>
<script src="tests.js"></script>
</body>
</html>

View File

@ -2,19 +2,22 @@ window.jasmineRequire = require('./jasmine-2.1.3/jasmine');
require('./jasmine-2.1.3/jasmine-html');
require('./jasmine-2.1.3/boot');
var consoleReporter = require('./jasmine-2.1.3/console');
var app = require('remote').require('app');
jasmine.getEnv().addReporter(new consoleReporter.ConsoleReporter()({
showColors: true,
timer: new jasmine.Timer(),
print: function() {
console.log(arguments);
process.stdout.write.apply(process.stdout, arguments);
},
onComplete: function () {
app.quit();
}
}));
var fs = require('fs');
var tests = fs.readdirSync('./specs').filter(function (f) {
return f.indexOf('-spec') !== -1;
var tests = fs.readdirSync('./tests').filter(function (f) {
return f.indexOf('-' + process.env.TEST_TYPE) !== -1;
});
tests.forEach(function (t) {