Merge pull request #177 from kitematic/setup-cancel

Install docker and boot2docker with Kitematic
This commit is contained in:
Jeffrey Morgan 2015-02-13 10:36:14 -08:00
commit e8f87e1ebe
10 changed files with 162 additions and 107 deletions

View File

@ -1,3 +1,7 @@
language: node_js
node_js:
- 0.10
cache:
directories:
- node_modules
- resources

View File

@ -12,7 +12,7 @@ describe('SetupStore', function () {
pit('downloads virtualbox if it is not installed', function () {
virtualBox.installed.mockReturnValue(false);
setupUtil.download.mockReturnValue(Promise.resolve());
return setupStore.steps().downloadVirtualBox.run().then(() => {
return setupStore.steps().download.run().then(() => {
// TODO: make sure download was called with the right args
expect(setupUtil.download).toBeCalled();
});
@ -23,30 +23,18 @@ describe('SetupStore', function () {
virtualBox.version.mockReturnValue(Promise.resolve('4.3.16'));
setupUtil.compareVersions.mockReturnValue(-1);
setupUtil.download.mockReturnValue(Promise.resolve());
return setupStore.steps().downloadVirtualBox.run().then(() => {
return setupStore.steps().download.run().then(() => {
expect(setupUtil.download).toBeCalled();
});
});
pit('skips download if virtualbox is already installed', function () {
virtualBox.installed.mockReturnValue(true);
virtualBox.version.mockReturnValue(Promise.resolve('4.3.20'));
setupUtil.download.mockClear();
setupUtil.download.mockReturnValue(Promise.resolve());
setupUtil.compareVersions.mockReturnValue(1);
return setupStore.steps().downloadVirtualBox.run().then(() => {
expect(setupUtil.download).not.toBeCalled();
});
});
});
describe('install step', function () {
pit('installs virtualbox if it is not installed', function () {
virtualBox.installed.mockReturnValue(false);
virtualBox.killall.mockReturnValue(Promise.resolve());
setupUtil.isSudo.mockReturnValue(Promise.resolve(false));
util.exec.mockReturnValue(Promise.resolve());
return setupStore.steps().installVirtualBox.run().then(() => {
return setupStore.steps().install.run().then(() => {
// TODO: make sure that the right install command was executed
expect(util.exec).toBeCalled();
});
@ -56,29 +44,14 @@ describe('SetupStore', function () {
virtualBox.installed.mockReturnValue(true);
virtualBox.version.mockReturnValue(Promise.resolve('4.3.16'));
virtualBox.killall.mockReturnValue(Promise.resolve());
setupUtil.isSudo.mockReturnValue(Promise.resolve(false));
setupUtil.compareVersions.mockReturnValue(-1);
util.exec.mockReturnValue(Promise.resolve());
return setupStore.steps().installVirtualBox.run().then(() => {
return setupStore.steps().install.run().then(() => {
// TODO: make sure the right install command was executed
expect(virtualBox.killall).toBeCalled();
expect(util.exec).toBeCalled();
});
});
pit('skips install if virtualbox is already installed', function () {
virtualBox.installed.mockReturnValue(true);
virtualBox.version.mockReturnValue(Promise.resolve('4.3.20'));
setupUtil.isSudo.mockReturnValue(Promise.resolve(false));
setupUtil.compareVersions.mockReturnValue(-1);
util.exec.mockReturnValue(Promise.resolve());
return setupStore.steps().installVirtualBox.run().then(() => {
virtualBox.killall.mockClear();
util.exec.mockClear();
expect(virtualBox.killall).not.toBeCalled();
expect(util.exec).not.toBeCalled();
});
});
});
describe('init step', function () {
@ -86,7 +59,7 @@ describe('SetupStore', function () {
pit('inintializes the boot2docker vm if it does not exist', function () {
boot2docker.exists.mockReturnValue(Promise.resolve(false));
boot2docker.init.mockReturnValue(Promise.resolve());
return setupStore.steps().initBoot2Docker.run().then(() => {
return setupStore.steps().init.run().then(() => {
expect(boot2docker.init).toBeCalled();
});
});
@ -98,7 +71,7 @@ describe('SetupStore', function () {
boot2docker.stop.mockReturnValue(Promise.resolve());
boot2docker.upgrade.mockReturnValue(Promise.resolve());
setupUtil.compareVersions.mockReturnValue(-1);
return setupStore.steps().initBoot2Docker.run().then(() => {
return setupStore.steps().init.run().then(() => {
boot2docker.init.mockClear();
expect(boot2docker.init).not.toBeCalled();
expect(boot2docker.upgrade).toBeCalled();
@ -111,7 +84,7 @@ describe('SetupStore', function () {
boot2docker.status.mockReturnValue(false);
boot2docker.waitstatus.mockReturnValue(Promise.resolve());
boot2docker.start.mockReturnValue(Promise.resolve());
return setupStore.steps().startBoot2Docker.run().then(() => {
return setupStore.steps().start.run().then(() => {
expect(boot2docker.start).toBeCalled();
});
});

View File

@ -17,7 +17,7 @@ var _logs = {};
var _streams = {};
var _muted = {};
var ContainerStore = assign(EventEmitter.prototype, {
var ContainerStore = assign(Object.create(EventEmitter.prototype), {
CLIENT_CONTAINER_EVENT: 'client_container_event',
CLIENT_RECOMMENDED_EVENT: 'client_recommended_event',
SERVER_CONTAINER_EVENT: 'server_container_event',

View File

@ -8,8 +8,10 @@ var docker = require('./Docker');
var router = require('./router');
var boot2docker = require('./boot2docker');
var ContainerStore = require('./ContainerStore');
var SetupStore = require('./ContainerStore');
var SetupStore = require('./SetupStore');
var settingsjson;
var Menu = require('./Menu');
try {
settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8'));
} catch (err) {
@ -34,6 +36,7 @@ bugsnag.appVersion = app.getVersion();
router.run(Handler => React.render(<Handler/>, document.body));
if (!window.location.hash.length || window.location.hash === '#/') {
SetupStore.run().then(boot2docker.ip).then(ip => {
console.log(ip);
docker.setHost(ip);
ContainerStore.init(function (err) {
if (err) { console.log(err); }

View File

@ -1,8 +1,11 @@
var remote = require('remote');
var app = remote.require('app');
var path = require('path');
var docker = require('./Docker');
var Menu = remote.require('menu');
var BrowserWindow = remote.require('browser-window');
var router = require('./Router');
var util = require('./Util');
// main.js
var template = [
@ -59,6 +62,28 @@ var template = [
},
]
},
{
label: 'File',
submenu: [
{
label: 'New Container',
accelerator: 'Command+N',
selector: 'undo:'
},
{
type: 'separator'
},
{
label: 'Open Docker Terminal',
accelerator: 'Command+Shift+T',
click: function() {
var terminal = path.join(process.cwd(), 'resources', 'terminal');
var cmd = [terminal, `DOCKER_HOST=${'tcp://' + docker.host + ':2376'} DOCKER_CERT_PATH=${path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.boot2docker/certs/boot2docker-vm')} DOCKER_TLS_VERIFY=1 $SHELL`];
util.exec(cmd).then(() => {});
}
},
]
},
{
label: 'Edit',
submenu: [

View File

@ -83,7 +83,7 @@ var Setup = React.createClass({
<div className="content">
<h4>Installation Cancelled</h4>
<h1>Couldn&#39;t Install VirtualBox</h1>
<p>Kitematic did not receive the administrative privileges required to install VirtualBox.</p>
<p>Kitematic did not receive the administrative privileges required to install VirtualBox &amp; Docker.</p>
<p>Please retry or download &amp; install VirutalBox manually from the <a onClick={this.handleOpenWebsite}>official Oracle website</a>.</p>
<button className="btn btn-action" onClick={this.handleRetry}>Retry</button>
</div>

View File

@ -1,5 +1,4 @@
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var _ = require('underscore');
var path = require('path');
var Promise = require('bluebird');
@ -7,70 +6,60 @@ var boot2docker = require('./Boot2Docker');
var virtualBox = require('./VirtualBox');
var setupUtil = require('./SetupUtil');
var util = require('./Util');
var assign = require('object-assign');
var SUDO_PROMPT = 'Kitematic requires administrative privileges to install VirtualBox.';
var _currentStep = null;
var _error = null;
var _cancelled = false;
var _retryPromise = null;
var _requiredSteps = [];
var _steps = [{
name: 'downloadVirtualBox',
name: 'download',
title: 'Downloading VirtualBox',
message: 'VirtualBox is being downloaded. Kitematic requires VirtualBox to run containers.',
totalPercent: 35,
percent: 0,
run: Promise.coroutine(function* (progressCallback) {
var packagejson = util.packagejson();
if (virtualBox.installed()) {
var version = yield virtualBox.version();
if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) >= 0) {
return;
}
}
var virtualBoxFile = `https://github.com/kitematic/virtualbox/releases/download/${packagejson['virtualbox-version']}/${packagejson['virtualbox-filename']}`;
yield setupUtil.download(virtualBoxFile, path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']), packagejson['virtualbox-checksum'], percent => {
yield setupUtil.download(virtualBoxFile, path.join(util.supportDir(), packagejson['virtualbox-filename']), packagejson['virtualbox-checksum'], percent => {
progressCallback(percent);
});
})
}, {
name: 'installVirtualBox',
title: 'Installing VirtualBox',
message: "VirtualBox is being installed in the background. We may need you to type in your password to continue.",
name: 'install',
title: 'Installing Docker & VirtualBox',
message: 'VirtualBox is being installed in the background. We may need you to type in your password to continue.',
totalPercent: 5,
percent: 0,
seconds: 5,
run: Promise.coroutine(function* () {
var packagejson = util.packagejson();
if (virtualBox.installed()) {
var version = yield virtualBox.version();
if (setupUtil.compareVersions(version, packagejson['virtualbox-required-version']) >= 0) {
return;
}
var base = util.copyBinariesCmd() + ' && ' + util.fixBinariesCmd();
if (!virtualBox.installed() || setupUtil.compareVersions(yield virtualBox.version(), packagejson['virtualbox-required-version']) < 0) {
yield virtualBox.killall();
base += ` && installer -pkg ${path.join(util.supportDir(), packagejson['virtualbox-filename'])} -target /`;
}
var isSudo = yield setupUtil.isSudo();
var iconPath = path.join(setupUtil.resourceDir(), 'kitematic.icns');
var sudoCmd = isSudo ? ['sudo'] : [path.join(setupUtil.resourceDir(), 'cocoasudo'), '--icon=' + iconPath, `--prompt=${SUDO_PROMPT}`];
sudoCmd.push.apply(sudoCmd, ['installer', '-pkg', path.join(setupUtil.supportDir(), packagejson['virtualbox-filename']), '-target', '/']);
var cmd = `${util.escapePath(path.join(util.resourceDir(), 'cocoasudo'))} --prompt="Kitematic requires administrative privileges to install VirtualBox." bash -c \"${base}\"`;
try {
yield util.exec(sudoCmd);
yield util.exec(cmd);
} catch (err) {
throw null;
}
})
}, {
name: 'initBoot2Docker',
name: 'init',
title: 'Setting up Docker VM',
message: "To run Docker containers on your computer, we are setting up a Linux virtual machine provided by boot2docker.",
message: 'To run Docker containers on your computer, we are setting up a Linux virtual machine provided by boot2docker.',
totalPercent: 15,
percent: 0,
seconds: 11,
run: Promise.coroutine(function* (progressCallback) {
setupUtil.simulateProgress(this.seconds, progressCallback);
yield virtualBox.vmdestroy('kitematic-vm');
var exists = yield boot2docker.exists();
if (!exists) {
setupUtil.simulateProgress(this.seconds, progressCallback);
yield boot2docker.init();
return;
}
@ -81,21 +70,20 @@ var _steps = [{
var isoversion = boot2docker.isoversion();
if (!isoversion || setupUtil.compareVersions(isoversion, boot2docker.version()) < 0) {
setupUtil.simulateProgress(this.seconds, progressCallback);
yield boot2docker.stop();
yield boot2docker.upgrade();
}
})
}, {
name: 'startBoot2Docker',
name: 'start',
title: 'Starting Docker VM',
message: "Kitematic is starting the boot2docker VM. This may take about a minute.",
totalPercent: 45,
percent: 0,
seconds: 35,
run: function (progressCallback) {
setupUtil.simulateProgress(this.seconds, progressCallback);
return boot2docker.waitstatus('saving').then(boot2docker.status).then(status => {
setupUtil.simulateProgress(this.seconds, progressCallback);
if (status !== 'running') {
return boot2docker.start();
}
@ -103,28 +91,29 @@ var _steps = [{
}
}];
var SetupStore = assign(EventEmitter.prototype, {
var SetupStore = assign(Object.create(EventEmitter.prototype), {
PROGRESS_EVENT: 'setup_progress',
STEP_EVENT: 'setup_step',
ERROR_EVENT: 'setup_error',
step: function () {
return _currentStep || _steps[0];
return _currentStep;
},
steps: function () {
return _.indexBy(_steps, 'name');
},
stepCount: function () {
return _steps.length;
return _requiredSteps.length;
},
number: function () {
return _.indexOf(_steps, _currentStep) + 1;
return _.indexOf(_requiredSteps, _currentStep) + 1;
},
percent: function () {
var total = 0;
_.each(_steps, step => {
total += step.totalPercent * step.percent / 100;
var sofar = 0;
var totalPercent = _requiredSteps.reduce((prev, step) => prev + step.totalPercent, 0);
_.each(_requiredSteps, step => {
sofar += step.totalPercent * step.percent / 100;
});
return Math.min(Math.round(total), 99);
return Math.min(Math.round(100 * sofar / totalPercent), 99);
},
error: function () {
return _error;
@ -141,13 +130,37 @@ var SetupStore = assign(EventEmitter.prototype, {
_retryPromise = Promise.defer();
return _retryPromise.promise;
},
run: Promise.coroutine(function* () {
init: Promise.coroutine(function* () {
var packagejson = util.packagejson();
var isoversion = boot2docker.isoversion();
var required = {};
required.download = !virtualBox.installed() || setupUtil.compareVersions(yield virtualBox.version(), packagejson['virtualbox-required-version']) < 0;
required.install = required.download || setupUtil.needsBinaryFix();
required.init = !(yield boot2docker.exists()) || !isoversion || setupUtil.compareVersions(isoversion, boot2docker.version()) < 0;
required.start = required.init || (yield boot2docker.status()) !== 'running';
var exists = yield boot2docker.exists();
if (exists) {
this.steps().startBoot2Docker.seconds = 13;
this.steps().start.seconds = 13;
}
for (let step of _steps) {
_requiredSteps = _steps.filter(function (step) {
return required[step.name];
});
}),
updateBinaries: function () {
if (setupUtil.needsBinaryFix()) {
return Promise.resolve();
}
if (setupUtil.shouldUpdateBinaries()) {
return util.exec(util.copyBinariesCmd());
}
return Promise.resolve();
},
run: Promise.coroutine(function* () {
yield this.init();
yield this.updateBinaries();
for (let step of _requiredSteps) {
_currentStep = step;
step.percent = 0;
while (true) {
@ -163,6 +176,7 @@ var SetupStore = assign(EventEmitter.prototype, {
break;
} catch (err) {
if (err) {
console.log(err.stack);
_error = err;
this.emit(this.ERROR_EVENT);
} else {

View File

@ -1,36 +1,33 @@
var _ = require('underscore');
var crypto = require('crypto');
var exec = require('exec');
var fs = require('fs');
var path = require('path');
var request = require('request');
var progress = require('request-progress');
var path = require('path');
var Promise = require('bluebird');
var util = require('./Util');
var SetupUtil = {
supportDir: function () {
var dirs = ['Library', 'Application\ Support', 'Kitematic'];
var acc = process.env.HOME;
dirs.forEach(function (d) {
acc = path.join(acc, d);
if (!fs.existsSync(acc)) {
fs.mkdirSync(acc);
}
});
return acc;
needsBinaryFix: function () {
if (!fs.existsSync('/usr/local/bin/docker') && !fs.existsSync('/usr/local/bin/boot2docker')) {
return fs.statSync('/usr/local/bin').gid !== 80 || fs.statSync('/usr/local/bin').uid !== process.getuid();
}
if (fs.existsSync('/usr/local/bin/docker') && (fs.statSync('/usr/local/bin/docker').gid !== 80 || fs.statSync('/usr/local/bin/docker').uid !== process.getuid())) {
return true;
}
if (fs.existsSync('/usr/local/bin/boot2docker') && (fs.statSync('/usr/local/bin/boot2docker').gid !== 80 || fs.statSync('/usr/local/bin/boot2docker').uid !== process.getuid())) {
return true;
}
return false;
},
resourceDir: function () {
return process.env.RESOURCES_PATH;
},
isSudo: function () {
return new Promise((resolve, reject) => {
exec(['sudo', '-n', '-u', 'root', 'true'], (stderr, stdout, code) => {
if (code) {
reject(stderr);
}
resolve(stderr.indexOf('a password is required') === -1);
});
});
shouldUpdateBinaries: function () {
var packagejson = util.packagejson();
return !fs.existsSync('/usr/local/bin/docker') ||
!fs.existsSync('/usr/local/bin/boot2docker') ||
this.checksum('/usr/local/bin/boot2docker') !== this.checksum(path.join(util.resourceDir(), 'boot2docker-' + packagejson['boot2docker-version'])) ||
this.checksum('/usr/local/bin/docker') !== this.checksum(path.join(util.resourceDir(), 'docker-' + packagejson['docker-version']));
},
simulateProgress: function (estimateSeconds, progress) {
var times = _.range(0, estimateSeconds * 1000, 200);
@ -42,10 +39,13 @@ var SetupUtil = {
timers.push(timer);
});
},
checksum: function (filename) {
return crypto.createHash('sha256').update(fs.readFileSync(filename), 'utf8').digest('hex');
},
download: function (url, filename, checksum, percentCallback) {
return new Promise((resolve, reject) => {
if (fs.existsSync(filename)) {
var existingChecksum = crypto.createHash('sha256').update(fs.readFileSync(filename), 'utf8').digest('hex');
var existingChecksum = this.checksum(filename);
if (existingChecksum === checksum) {
resolve();
return;

View File

@ -4,9 +4,11 @@ var fs = require('fs');
var path = require('path');
module.exports = {
exec: function (args) {
exec: function (args, options) {
options = options || {};
return new Promise((resolve, reject) => {
exec(args, (stderr, stdout, code) => {
console.log(options);
exec(args, options, (stderr, stdout, code) => {
if (code) {
reject(stderr);
}
@ -17,7 +19,42 @@ module.exports = {
home: function () {
return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
},
supportDir: function () {
var dirs = ['Library', 'Application\ Support', 'Kitematic'];
var acc = process.env.HOME;
dirs.forEach(function (d) {
acc = path.join(acc, d);
if (!fs.existsSync(acc)) {
fs.mkdirSync(acc);
}
});
return acc;
},
resourceDir: function () {
return process.env.RESOURCES_PATH;
},
packagejson: function () {
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
},
copycmd: function (src, dest) {
return ['rm', '-f', dest, '&&', 'cp', src, dest];
},
copyBinariesCmd: function () {
var packagejson = this.packagejson();
var cmd = [];
cmd.push.apply(cmd, this.copycmd(this.escapePath(path.join(this.resourceDir(), 'boot2docker-' + packagejson['boot2docker-version'])), '/usr/local/bin/boot2docker'));
cmd.push('&&');
cmd.push.apply(cmd, this.copycmd(this.escapePath(path.join(this.resourceDir(), 'docker-' + packagejson['docker-version'])), '/usr/local/bin/docker'));
return cmd.join(' ');
},
fixBinariesCmd: function () {
var cmd = [];
cmd.push.apply(cmd, ['chown', `${process.getuid()}:${80}`, this.escapePath(path.join('/usr/local/bin', 'boot2docker'))]);
cmd.push('&&');
cmd.push.apply(cmd, ['chown', `${process.getuid()}:${80}`, this.escapePath(path.join('/usr/local/bin', 'docker'))]);
return cmd.join(' ');
},
escapePath: function (str) {
return str.replace(/ /g, '\\ ').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}
};

View File

@ -27,13 +27,12 @@ var VirtualBox = {
return util.exec(this.command() + ' list runningvms | sed -E \'s/.*\\{(.*)\\}/\\1/\' | xargs -L1 -I {} ' + this.command() + ' controlvm {} poweroff');
},
killall: function () {
if (!this.installed()) {
return Promise.reject('VirtualBox not installed.');
}
return this.poweroffall().then(() => {
return util.exec(['pkill', 'VirtualBox']);
}).then(() => {
return util.exec(['pkill', 'VBox']);
}).catch(err => {
});
},
vmstate: function (name) {