Merge branch 'master' into backend-refactor

This commit is contained in:
Sean Li 2014-09-02 22:53:38 -07:00
commit d58e5f14d6
22 changed files with 665 additions and 407 deletions

View File

@ -80,7 +80,7 @@ Example:
Feature requests are welcome. But take a moment to find out whether your idea Feature requests are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. A roadmap of the project is kept fits with the scope and aims of the project. A roadmap of the project is kept
on the [Kitematic Roadmap](https://trello.com/b/xea5AHRk/kitematic-roadmap) Trello board. on the [Kitematic Roadmap](https://trello.com/b/G5Aw0Rqc/kitematic-roadmap) Trello board.
It's up to *you* to make a strong case to convince the project's developers of It's up to *you* to make a strong case to convince the project's developers of
the merits of this feature. Please provide as much detail and context as possible. the merits of this feature. Please provide as much detail and context as possible.

View File

@ -3,7 +3,7 @@
**Note:** If the installer gets stuck at any step for more than 1 minute, there is probably an error. Please help us troubleshoot by running it from the command line, and submit the logs to [contact@kitematic.com](mailto:contact@kitematic.com). **Note:** If the installer gets stuck at any step for more than 1 minute, there is probably an error. Please help us troubleshoot by running it from the command line, and submit the logs to [contact@kitematic.com](mailto:contact@kitematic.com).
1. `cd <dir with Kitematic.app>` 1. `cd <dir with Kitematic.app>`
2. Run `./Kitematic.app/Contents/MacOS/node-webkit` 2. Run `./Kitematic.app/Contents/MacOS/node-webkit`
Kitematic is still in Beta. Any effort in helping us find issues and improving the experience is greatly appreciated! Kitematic is still in Beta. Any effort in helping us find issues and improving the experience is greatly appreciated!
@ -45,7 +45,7 @@ Kitematic is still in Beta. Any effort in helping us find issues and improving t
- Remove VirtualBox - Remove VirtualBox
- rm /usr/local/bin/boot2docker - rm /usr/local/bin/boot2docker
- sudo route delete 172.17.0.0/16 192.168.59.103 (disable routing to containers through VM) - sudo route delete -net 172.17.0.0 -netmask 255.255.0.0 -gateway 192.168.60.103 (disable routing to containers through VM)
- rm -rf ~/Library/Application\ Support/Kitematic (remove app data) - rm -rf ~/Library/Application\ Support/Kitematic (remove app data)
- rm /Library/LaunchAgents/com.kitematic.route.plist (remove launch job that sets up routing to the containers) - rm /Library/LaunchAgents/com.kitematic.route.plist (remove launch job that sets up routing to the containers)
@ -61,7 +61,7 @@ Kitematic's documentation and other information can be found at [http://kitemati
Please read through our [Contributing Guidelines](https://github.com/kitematic/kitematic/blob/master/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. Please read through our [Contributing Guidelines](https://github.com/kitematic/kitematic/blob/master/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development.
Development [Roadmap](https://trello.com/b/xea5AHRk/kitematic-roadmap) can be found on our Trello board. Development [Roadmap](https://trello.com/b/G5Aw0Rqc/kitematic-roadmap) can be found on our Trello board.
## Community ## Community
@ -74,7 +74,7 @@ Keep track of development and community news.
## Versioning ## Versioning
For transparency into our release cycle and in striving to maintain backward compatibility, Kitematic is maintained under the [Semantic Versioning Guidelines](http://semver.org/). We'll try very hard adhere to those rules whenever possible. For transparency into our release cycle and in striving to maintain backward compatibility, Kitematic is maintained under the [Semantic Versioning Guidelines](http://semver.org/). We'll try very hard to adhere to those rules whenever possible.
## Creators ## Creators

View File

@ -43,10 +43,7 @@ var start = function (callback) {
freeport(function(err, mongoPort) { freeport(function(err, mongoPort) {
console.log('MongoDB: ' + mongoPort); console.log('MongoDB: ' + mongoPort);
console.log('webPort: ' + webPort); console.log('webPort: ' + webPort);
child_process.exec('kill $(ps aux -e | grep PURPOSE=KITEMATIC | awk \'{print $2}\')', function (error, stdout, stderr) { child_process.exec('kill $(ps aux -e | grep PURPOSE=KITEMATIC | awk \'{print $2}\') && rm ' + path.join(dataPath, 'mongod.lock'), function (error, stdout, stderr) {
console.log(error);
console.log(stdout);
console.log(stderr);
var mongoChild = child_process.spawn(path.join(process.cwd(), 'resources', 'mongod'), ['--bind_ip', '127.0.0.1', '--dbpath', dataPath, '--port', mongoPort, '--unixSocketPrefix', dataPath], { var mongoChild = child_process.spawn(path.join(process.cwd(), 'resources', 'mongod'), ['--bind_ip', '127.0.0.1', '--dbpath', dataPath, '--port', mongoPort, '--unixSocketPrefix', dataPath], {
env: { env: {
PURPOSE: 'KITEMATIC' PURPOSE: 'KITEMATIC'
@ -54,7 +51,12 @@ var start = function (callback) {
}); });
var started = false; var started = false;
mongoChild.stdout.setEncoding('utf8'); mongoChild.stdout.setEncoding('utf8');
mongoChild.stderr.setEncoding('utf8');
mongoChild.stderr.on('data', function (data) {
console.log(data);
});
mongoChild.stdout.on('data', function (data) { mongoChild.stdout.on('data', function (data) {
console.log(data);
if (data.indexOf('waiting for connections on port ' + mongoPort)) { if (data.indexOf('waiting for connections on port ' + mongoPort)) {
if (!started) { if (!started) {
started = true; started = true;

View File

@ -166,6 +166,10 @@
// Server and Client // Server and Client
"Docker": true, "Docker": true,
"Util": true, "Util": true,
"Boot2Docker": true,
"Installer": true,
"VirtualBox": true,
"boot2dockerexec": true, "boot2dockerexec": true,
"getBoot2DockerIp": true, "getBoot2DockerIp": true,
"getBoot2DockerState": true, "getBoot2DockerState": true,

View File

@ -1,14 +1,54 @@
var exec = require('exec'); var exec = require('exec');
var path = require('path'); var path = require('path');
boot2dockerexec = function (command, callback) { Boot2Docker = {};
exec(path.join(Util.getBinDir(), 'boot2docker') + ' ' + command, function(err, stdout) {
callback(err, stdout); Boot2Docker.REQUIRED_IP = '192.168.60.103';
Boot2Docker.exec = function (command, callback) {
exec(path.join(Util.getBinDir(), 'boot2docker') + ' ' + command, function(err, stdout, stderr) {
callback(err, stdout, stderr);
}); });
}; };
getBoot2DockerIp = function (callback) { Boot2Docker.exists = function (callback) {
boot2dockerexec('ip', function (err, stdout) { this.exec('info', function (err) {
if (err) {
callback(null, false);
} else {
callback(null, true);
}
});
};
Boot2Docker.stop = function (callback) {
this.exec('stop', function (err, stdout) {
if (err) {
callback(err);
} else {
callback(null);
}
});
};
Boot2Docker.erase = function (callback) {
var VMFileLocation = path.join(Util.getHomePath(), 'VirtualBox\\ VMs/boot2docker-vm');
exec('rm -rf ' + VMFileLocation, function (err) {
callback(err);
});
};
Boot2Docker.upgrade = function (callback) {
var self = this;
self.stop(function (err) {
self.exec('upgrade', function (err, stdout) {
callback(err);
});
});
};
Boot2Docker.ip = function (callback) {
this.exec('ip', function (err, stdout) {
if (err) { if (err) {
callback(err, null); callback(err, null);
} else { } else {
@ -17,12 +57,54 @@ getBoot2DockerIp = function (callback) {
}); });
}; };
getBoot2DockerState = function (callback) { Boot2Docker.setIp = function (ifname, ip, callback) {
boot2dockerexec(' info', function (err, stdout) { this.exec('ssh "sudo ifconfig ' + ifname + ' ' + ip + ' netmask 255.255.255.0"', function (err, stdout) {
if (err) { callback(err);
callback(err, null); });
};
Boot2Docker.init = function (callback) {
this.exec('init', function (err) {
callback(err);
});
};
Boot2Docker.start = function (callback) {
var self = this;
self.exists(function (err, exists) {
if (!exists) {
callback('Cannot start if the boot2docker VM doesn\'t exist');
return; return;
} }
self.exec('up -v', function (err, stdout) {
// Sometimes boot2docker returns an error code even though it's working / waiting, so treat that as
// Success as well
if (!err || (err.indexOf('Waiting for VM to be started') !== -1 || err.indexOf('..........') !== -1)) {
self.correct(function (err) {
if (err) { callback(err); return; }
self.injectUtilities(function (err) {
callback(err);
});
});
} else {
callback(err);
}
});
});
};
Boot2Docker.correct = function (callback) {
Boot2Docker.setIp('eth2', Boot2Docker.REQUIRED_IP, function(err) {
if (err) { callback(err); return; }
VirtualBox.removeDHCP(function (err) {
callback();
});
});
};
Boot2Docker.state = function (callback) {
this.exec('info', function (err, stdout, stderr) {
if (err) { callback(err, null); return; }
try { try {
var info = JSON.parse(stdout); var info = JSON.parse(stdout);
callback(null, info.State); callback(null, info.State);
@ -32,8 +114,8 @@ getBoot2DockerState = function (callback) {
}); });
}; };
getBoot2DockerDiskUsage = function (callback) { Boot2Docker.diskUsage = function (callback) {
boot2dockerexec('ssh "df"', function (err, stdout) { this.exec('ssh "df"', function (err, stdout) {
if (err) { if (err) {
callback(err, null); callback(err, null);
return; return;
@ -61,8 +143,8 @@ getBoot2DockerDiskUsage = function (callback) {
}); });
}; };
getBoot2DockerMemoryUsage = function (callback) { Boot2Docker.memoryUsage = function (callback) {
boot2dockerexec('ssh "free -m"', function (err, stdout) { this.exec('ssh "free -m"', function (err, stdout) {
if (err) { if (err) {
callback(err, null); callback(err, null);
return; return;
@ -92,178 +174,105 @@ getBoot2DockerMemoryUsage = function (callback) {
}); });
}; };
getBoot2DockerInfo = function (callback) { Boot2Docker.stats = function (callback) {
boot2dockerexec('ssh "sudo ifconfig eth1 192.168.59.103 netmask 255.255.255.0"', function (err, stdout) { var self = this;
exec('VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0', function (err, stdout) { self.state(function (err, state) {
getBoot2DockerState(function (err, state) { if (err) { callback(err, null); return; }
if (state === 'poweroff') {
callback(null, {state: state});
return;
}
self.memoryUsage(function (err, mem) {
if (err) { if (err) {
callback(err, null); callback(null, {state: state});
return; return;
} }
if (state === 'poweroff') { self.diskUsage(function (err, disk) {
callback(null, {state: state});
} else {
getBoot2DockerMemoryUsage(function (err, mem) {
if (err) { callback(null, {state: state}); }
getBoot2DockerDiskUsage(function (err, disk) {
if (err) { callback(null, {state: state}); }
callback(null, {
state: state,
memory: mem,
disk: disk
});
});
});
}
});
});
});
};
boot2DockerVMExists = function (callback) {
boot2dockerexec('info', function (err) {
if (err) {
callback(null, false);
} else {
callback(null, true);
}
});
};
eraseBoot2DockerVMFiles = function (callback) {
var VMFileLocation = path.join(Util.getHomePath(), 'VirtualBox\\ VMs/boot2docker-vm');
exec('rm -rf ' + VMFileLocation, function (err) {
callback(err);
});
};
initBoot2Docker = function (callback) {
isVirtualBoxInstalled(function (err, installed) {
if (err) {
callback(err);
return;
}
if (installed) {
boot2dockerexec('init', function (err) {
console.log(err);
if (err) { if (err) {
if (err.indexOf('exit status 1') !== -1) { callback(null, {state: state, memory: mem});
eraseBoot2DockerVMFiles(function () { return;
boot2dockerexec('init', function (err) {
callback(err);
});
});
} else {
callback(err);
}
} else {
callback();
} }
callback(null, {
state: state,
memory: mem,
disk: disk
});
}); });
});
});
};
/**
* Get the VM's version.
* Node that this only works if the VM is up and running.
*/
Boot2Docker.vmVersion = function (callback) {
this.exec('ssh "cat /etc/version', function (err, stdout, stderr) {
if (err) {
callback(err);
return;
} else { } else {
callback(new Error('initBoot2Docker called but VirtualBox isn\'t installed.')); callback(null, stdout);
} }
}); });
}; };
upgradeBoot2Docker = function (callback) { Boot2Docker.version = function (callback) {
boot2dockerexec('upgrade', function (err, stdout) { this.exec('version', function (err, stdout, stderr) {
console.log(stdout); if (err) {
callback(err); callback(err);
return;
}
var match = stdout.match(/Client version: v(\d\.\d\.\d)/);
if (!match || match.length < 2) {
callback('Could not parse the boot2docker cli version.');
} else {
callback(null, match[1]);
}
}); });
}; };
installBoot2DockerAddons = function (callback) { Boot2Docker.injectUtilities = function (callback) {
exec('/bin/cat ' + path.join(Util.getBinDir(), 'kite-binaries.tar.gz') + ' | ' + path.join(Util.getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) { exec('/bin/cat ' + path.join(Util.getBinDir(), 'kite-binaries.tar.gz') + ' | ' + path.join(Util.getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) {
console.log(stdout);
callback(err); callback(err);
}); });
boot2dockerexec('ssh "sudo ifconfig eth1 192.168.59.103 netmask 255.255.255.0"', function (err, stdout) {});
exec('VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0', function (err, stdout) {});
}; };
startBoot2Docker = function (callback) { Boot2Docker.check = function (callback) {
isVirtualBoxInstalled(function (err, installed) { var self = this;
if (err) { self.exists(function (err, exists) {
callback(err);
return;
}
if (installed) {
boot2DockerVMExists(function (err, exists) {
if (exists) {
boot2dockerexec('up -v', function (err, stdout) {
console.log(err);
console.log(stdout);
if (err) {
if (err.indexOf('Waiting for VM to be started') !== -1 || err.indexOf('..........') !== -1) {
installBoot2DockerAddons(function (err) {
callback(err);
});
} else {
callback(err);
}
} else {
installBoot2DockerAddons(function (err) {
callback(err);
});
}
});
} else {
callback(new Error('startBoot2Docker called but boot2docker-vm doesn\'t exist.'));
}
});
} else {
callback(new Error('startBoot2Docker called but VirtualBox isn\'t installed.'));
}
});
};
stopBoot2Docker = function (callback) {
boot2dockerexec('stop', function (err, stdout) {
console.log(stdout);
console.log(err);
if (err) {
callback(err);
return;
}
callback(null);
});
};
checkBoot2DockerVM = function (callback) {
boot2DockerVMExists(function (err) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} else { } else {
getBoot2DockerState(function (err, state) { self.state(function (err, state) {
if (state !== 'running') { if (state !== 'running') {
callback('boot2docker not running'); callback('boot2docker not running');
} else { } else {
callback(); self.correct(function (err) {
callback(err);
});
} }
}); });
} }
}); });
}; };
// Make sure the VM exists, is up and is running. Boot2Docker.resolve = function (callback) {
resolveBoot2DockerVM = function (callback) { var self = this;
boot2DockerVMExists(function (err, exists) { self.exists(function (err, exists) {
// If somehow the boot2docker VM doesn't exist anymor then re-create it. // If somehow the boot2docker VM doesn't exist anymor then re-create it.
if (!exists) { if (!exists) {
initBoot2Docker(function () { self.init(function () {
startBoot2Docker(function (err) { self.start(function (err) {
callback(err); callback(err);
}); });
}); });
} else { } else {
// If it exists but it's not running.. restart it. // If it exists but it's not running.. restart it.
getBoot2DockerState(function (err, state) { self.state(function (err, state) {
if (state !== 'running') { if (state !== 'running') {
startBoot2Docker(function (err) { self.start(function (err) {
callback(err); callback(err);
}); });
} else { } else {

View File

@ -0,0 +1,160 @@
var async = require('async');
Installer = {};
Installer.CURRENT_VERSION = '0.0.2';
Installer.isUpToDate = function () {
return !!Installs.findOne({version: Installer.CURRENT_VERSION});
};
/**
* Install steps. A step is a function that accepts a function (err) callback and returns once that step is complete.keys:
* - run: Function that runs the installation step and calls the callback with an error if failed.
* - pastMessage: Message to show after step completion
* - message: Message to show while step is running
* - imperativeMessage: Message to show before running
*/
Installer.steps = [
{
run: function (callback) {
var installed = VirtualBox.installed();
if (!installed) {
VirtualBox.install(function (err) {
callback(err);
});
} else {
// Version 4.3.12 is required.
VirtualBox.version(function (err, installedVersion) {
if (err) {
callback(err);
return;
}
var needsUpdate = Util.compareVersions(installedVersion, VirtualBox.REQUIRED_VERSION) < 0;
if (needsUpdate) {
VirtualBox.install(function (err) {
callback(err);
});
} else {
callback();
}
});
}
},
pastMessage: 'VirtualBox installed',
message: 'Installing VirtualBox',
futureMessage: 'Install VirtualBox if necessary'
},
// 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 {
Boot2Docker.stop(function (err) {
if (err) { callback(err); return; }
Boot2Docker.upgrade(function (err) {
callback(err);
});
});
}
});
},
pastMessage: 'Setup the Boot2Docker VM (if required)',
message: 'Setting up the Boot2Docker VM',
futureMessage: 'Set up the Boot2Docker VM(if required)'
},
{
run: function (callback) {
VirtualBox.addCustomHostAdapter('boot2docker-vm', function (err, ifname) {
callback(err);
});
},
pastMessage: 'Added custom host adapter to the Boot2Docker VM',
message: 'Adding custom host adapter to the Boot2Docker VM',
futureMessage: 'Add custom host adapter to the Boot2Docker VM'
},
// Start the Kitematic VM
{
run: function (callback) {
Boot2Docker.state(function (err, state) {
if (err) { callback(err); return; }
if (state !== 'running') {
console.log('starting');
Boot2Docker.start(function (err) {
callback(err);
});
} else {
Boot2Docker.setIp('eth2', Boot2Docker.REQUIRED_IP, function(err) {
callback(err);
});
}
});
},
pastMessage: 'Started the Boot2Docker VM',
message: 'Starting the Boot2Docker VM',
futureMessage: 'Start the Kitematic VM',
},
{
run: function (callback) {
VirtualBox.setupRouting('boot2docker-vm', function (err, ifname) {
callback(err);
});
},
pastMessage: 'Container routing set up',
message: 'Setting up container routing (root required)',
futureMessage: 'Set up container routing to VM (root required)'
},
// Set up the default Kitematic images
{
run: function (callback) {
Meteor.call('reloadDefaultContainers', function (err) {
callback(err);
});
},
pastMessage: 'Started the Boot2Docker VM',
message: 'Setting up the default Kitematic images...',
subMessage: '(This may take a few minutes)',
futureMessage: 'Set up the default Kitematic images',
}
];
Installer.run = function (callback) {
var currentStep = 0;
Session.set('currentInstallStep', currentStep);
Session.set('numberOfInstallSteps', this.steps.length);
async.eachSeries(this.steps, function (step, callback) {
console.log('Performing step ' + currentStep);
step.run(function (err) {
if (err) {
callback(err);
} else {
currentStep += 1;
Session.set('currentInstallStep', currentStep);
callback();
}
});
}, function (err) {
if (err) {
// if any of the steps fail
console.log('Kitematic setup failed at step ' + currentStep);
console.log(err);
Session.set('failedStep', currentStep);
Session.set('failedError', err);
callback(err);
} else {
// Setup Finished
console.log('Setup finished.');
callback();
}
});
};

View File

@ -48,12 +48,16 @@ Router.map(function () {
controller: 'SetupController', controller: 'SetupController',
action: function () { action: function () {
if (this.ready()) { if (this.ready()) {
var install = Installs.findOne(); if (!Installer.isUpToDate()) {
if (!install) { if (!Installs.findOne()) {
console.log('No installs detected, running installer again.'); console.log('No installs detected, running installer again.');
this.redirect('/setup/intro'); this.redirect('/setup/intro');
} else {
// There's an install but it's lower than the current version, re-run as an 'update'.
Session.set('isUpdating', true);
this.redirect('/setup/intro');
}
} else { } else {
startFixInterval();
this.redirect('/apps'); this.redirect('/apps');
} }
} }

View File

@ -1,54 +1,158 @@
var fs = require('fs'); var fs = require('fs');
var child_process = require('child_process'); var exec = require('exec');
var path = require('path'); var path = require('path');
isVirtualBoxInstalled = function (callback) { VirtualBox = {};
fs.exists('/usr/bin/VBoxManage', function (exists) {
callback(null, exists); VirtualBox.REQUIRED_VERSION = '4.3.12';
VirtualBox.INCLUDED_VERSION = '4.3.12';
VirtualBox.INSTALLER_FILENAME = 'virtualbox-4.3.12.pkg';
// Info for the hostonly interface we add to the VM.
VirtualBox.HOSTONLY_HOSTIP = '192.168.60.3';
VirtualBox.HOSTONLY_NETWORKMASK = '255.255.255.0';
VirtualBox.installed = function () {
return fs.existsSync('/usr/bin/VBoxManage');
};
VirtualBox.exec = function (command, callback) {
exec('/usr/bin/VBoxManage ' + command, function (error, stdout, stderr) {
callback(error, stdout, stderr);
}); });
}; };
isResolverSetup = function (callback) { VirtualBox.install = function (callback) {
fs.readFile('/etc/resolver/dev', { // -W waits for the process to close before finishing.
encoding: 'utf8' exec('open -W ' + path.join(Util.getBinDir(), this.INSTALLER_FILENAME), function (error, stdout, stderr) {
}, function (err, data) { if (error) {
callback(error);
return;
}
callback(null);
});
};
VirtualBox.version = function (callback) {
if (!this.installed()) {
callback('VirtualBox not installed.');
return;
}
this.exec('-v', function (err, stdout, stderr) {
if (err) { if (err) {
callback(err, false); callback(err);
} else { return;
if (data.indexOf('nameserver 172.17.42.1') !== -1) { }
callback(null, true); // Output is x.x.xryyyyyy
} else { var match = stdout.match(/(\d+\.\d+\.\d+).*/);
callback(null, false); if (!match || match.length < 2) {
callback('VBoxManage -v output format not recognized.');
return;
}
callback(null, match[1]);
});
};
VirtualBox.hostOnlyIfs = function (callback) {
this.exec('list hostonlyifs', function (err, stdout, stderr) {
if (err) {
callback(err);
return;
}
var lines = stdout.split('\n');
var hostOnlyIfs = {};
var currentIf = null;
_.each(lines, function (line) {
if (!line.length) {
return;
} }
var pieces = line.split(':');
var key = pieces[0].trim();
var value = pieces[1] ? pieces[1].trim() : null;
if (key === 'Name') {
currentIf = value;
hostOnlyIfs[value] = {};
}
hostOnlyIfs[currentIf][key] = value;
});
callback(null, hostOnlyIfs);
});
};
VirtualBox.hostOnlyAdapters = function (vm, callback) {
this.exec('showvminfo ' + vm + ' --machinereadable', function (err, stdout, stderr) {
if (err) {
callback(err);
return;
}
var matches = stdout.match(/(hostonlyadapter\d+)="(vboxnet\d+)"/g);
if (!matches.length) {
callback(null, {});
} else {
var objs = {};
_.each(matches, function (match) {
var pieces = match.split('=');
objs[pieces[0]] = pieces[1].replace(/"/g, '');
});
callback(null, objs);
} }
}); });
}; };
setupResolver = function (callback) { VirtualBox.hostOnlyAdapter = function (callback) {
var installFile = path.join(Util.getBinDir(), 'install'); var self = this;
var cocoaSudo = path.join(Util.getBinDir(), 'cocoasudo'); self.hostOnlyIfs(function (err, ifs) {
var execCommand = cocoaSudo + ' --prompt="Kitematic Setup wants to make changes. Type your password to allow this." ' + installFile; var iface = _.findWhere(_.toArray(ifs), {IPAddress: VirtualBox.HOSTONLY_HOSTIP});
child_process.exec(execCommand, function (error, stdout, stderr) { if (!iface) {
console.log(stdout); self.exec('hostonlyif create', function (err, stdout, stderr) {
if (error) { var match = stdout.match(/Interface '(vboxnet\d+)' was successfully created/);
console.log(error); if (!match) {
callback(error); callback('Could not parse output of hostonlyif create');
return; return;
}
self.exec('hostonlyif ipconfig ' + match[1] + ' --ip ' + VirtualBox.HOSTONLY_HOSTIP + ' --netmask ' + VirtualBox.HOSTONLY_NETWORKMASK, function(err, stdout, stderr) {
if (err) { callback(err); return; }
callback(null, match[1]);
});
});
} else {
callback(null, iface.Name);
} }
console.log('Virtualbox Installation & Resolver config complete.');
callback();
}); });
}; };
setupVirtualBox = function (callback) { VirtualBox.addCustomHostAdapter = function (vm, callback) {
child_process.exec('open -W ' + path.join(Util.getBinDir(), 'virtualbox-4.3.12.pkg'), function (error, stdout, stderr) { var self = this;
console.log(stdout); self.hostOnlyAdapter(function (err, ifname) {
if (error) { if (err) { callback(err); return; }
console.log(error); self.exec('modifyvm ' + vm + ' --nic3 hostonly --nictype3 virtio --cableconnected3 on --hostonlyadapter3 ' + ifname, function (err, stdout, stderr) {
callback(error); callback(err, ifname);
return; });
} });
console.log('Virtualbox Installation running.'); };
callback();
VirtualBox.setupRouting = function (vm, callback) {
// Get the host only adapter or create it if it doesn't exist
this.addCustomHostAdapter(vm, function (err, ifname) {
var installFile = path.join(Util.getBinDir(), 'install');
var cocoaSudo = path.join(Util.getBinDir(), 'cocoasudo');
var execCommand = cocoaSudo + ' --prompt="Kitematic needs your password to allow routing *.kite requests to containers." ' + installFile;
exec(execCommand, {env: {IFNAME: ifname, GATEWAY: Boot2Docker.REQUIRED_IP}}, function (error, stdout, stderr) {
if (error) {
callback(error);
return;
}
callback();
});
});
};
VirtualBox.removeDHCP = function (callback) {
var self = this;
self.hostOnlyAdapter(function (err, ifname) {
if (err) { callback(err); return; }
self.exec('dhcpserver remove --ifname ' + ifname, function (err, stdout, stderr) {
callback(err);
});
}); });
}; };

View File

@ -68,24 +68,11 @@ Meteor.call('getDockerHost', function (err, host) {
Session.set('dockerHost', host); Session.set('dockerHost', host);
}); });
updateBoot2DockerInfo = function () {
getBoot2DockerInfo(function (err, info) {
if (err) {
return;
}
Session.set('boot2dockerState', info.state);
if (info.state !== 'poweroff' && info.memory && info.disk) {
Session.set('boot2dockerMemoryUsage', info.memory);
Session.set('boot2dockerDiskUsage', info.disk);
}
});
};
fixBoot2DockerVM = function (callback) { fixBoot2DockerVM = function (callback) {
checkBoot2DockerVM(function (err) { Boot2Docker.check(function (err) {
if (err) { if (err) {
Session.set('available', false); Session.set('available', false);
resolveBoot2DockerVM(function (err) { Boot2Docker.resolve(function (err) {
if (err) { if (err) {
callback(err); callback(err);
} else { } else {
@ -138,28 +125,41 @@ fixDefaultContainers = function (callback) {
}; };
Meteor.setInterval(function () { Meteor.setInterval(function () {
updateBoot2DockerInfo(); Boot2Docker.exists(function (err, exists) {
if (err) { console.log(err); return; }
if (exists) {
Boot2Docker.state(function (err, state) {
if (err) { console.log(err); return; }
Session.set('boot2dockerState', state);
if (state === 'running') {
Boot2Docker.stats(function (err, stats) {
if (err) { console.log(err); return; }
if (stats.state !== 'poweroff' && stats.memory && stats.disk) {
Session.set('boot2dockerMemoryUsage', stats.memory);
Session.set('boot2dockerDiskUsage', stats.disk);
}
});
}
});
}
});
}, 5000); }, 5000);
fixInterval = null; Meteor.setInterval(function () {
startFixInterval = function () { if (Installer.isUpToDate()) {
stopFixInterval();
fixInterval = Meteor.setInterval(function () {
resolveWatchers(function () {}); resolveWatchers(function () {});
fixBoot2DockerVM(function (err) { if (!Session.get('boot2dockerOff')) {
if (err) { console.log(err); return; } fixBoot2DockerVM(function (err) {
Meteor.call('recoverApps');
fixDefaultImages(function (err) {
if (err) { console.log(err); return; } if (err) { console.log(err); return; }
fixDefaultContainers(function (err) { Meteor.call('recoverApps');
if (err) { console.log(err); } fixDefaultImages(function (err) {
if (err) { console.log(err); return; }
fixDefaultContainers(function (err) {
if (err) { console.log(err); }
});
}); });
}); });
}); }
}, 5000); }
}; }, 5000);
stopFixInterval = function () {
Meteor.clearInterval(fixInterval);
fixInterval = null;
};

View File

@ -3,18 +3,22 @@ Template.dashboard_settings.events({
var $btn = $(e.currentTarget); var $btn = $(e.currentTarget);
$btn.html('Starting Boot2Docker...'); $btn.html('Starting Boot2Docker...');
$btn.attr("disabled", "disabled"); $btn.attr("disabled", "disabled");
startFixInterval(); Session.set('boot2dockerOff', false);
startBoot2Docker(function (err) { Boot2Docker.start(function (err) {
if (err) { console.error(err); } if (err) {
console.log(err);
}
}); });
}, },
'click .btn-stop-boot2docker': function (e) { 'click .btn-stop-boot2docker': function (e) {
var $btn = $(e.currentTarget); var $btn = $(e.currentTarget);
$btn.html('Stopping Boot2Docker...'); $btn.html('Stopping Boot2Docker...');
$btn.attr("disabled", "disabled"); $btn.attr("disabled", "disabled");
stopFixInterval(); Session.set('boot2dockerOff', true);
stopBoot2Docker(function (err) { Boot2Docker.stop(function (err) {
if (err) { console.error(err); } if (err) {
console.log(err);
}
}); });
} }
}); });

View File

@ -1,141 +1,13 @@
var async = require('async');
// Install steps. A step is a function that accepts a function (err) callback and returns once that step is complete.
// keys:
// - install: Function that runs the installation step and calls the callback with an error if failed.
// - pastMessage: Message to show after step completion
// - message: Message to show while step is running
// - imperativeMessage: Message to show before running
var steps = [
// Set up VirtualBox
{
install: function (callback) {
isVirtualBoxInstalled(function (err, virtualBoxInstalled) {
if (!virtualBoxInstalled) {
setupVirtualBox(function (err) {
callback(err);
});
} else {
callback();
}
});
},
pastMessage: 'VirtualBox installed',
message: 'Installing VirtualBox',
futureMessage: 'Install VirtualBox if necessary'
},
// Set up the routing.
{
install: function (callback) {
setupResolver(function (err) {
callback(err);
});
},
pastMessage: 'Container routing set up (root required).',
message: 'Setting up container routing (root required).',
subMessage: '(This may take a few minutes)',
futureMessage: 'Set up container routing to VM (root required).'
},
// Set up the VM for running Kitematic apps
{
install: function (callback) {
console.log('Checking if vm exists...');
boot2DockerVMExists(function (err, exists) {
console.log('VM exists: ' + exists);
if (exists) {
console.log('Stopping vm');
stopBoot2Docker(function () {
console.log('Upgrading vm');
upgradeBoot2Docker(function () {
callback();
});
});
} else {
console.log('init VM');
initBoot2Docker(function () {
callback();
});
}
});
},
pastMessage: 'Set up the Kitematic VM',
message: 'Setting up the Kitematic VM...',
futureMessage: 'Set up the Kitematic VM'
},
// Start the Kitematic VM
{
install: function (callback) {
startBoot2Docker(function (err) {
callback(err);
});
},
pastMessage: 'Started the Kitematic VM',
message: 'Starting the Kitematic VM',
subMessage: '(This may take a few minutes)',
futureMessage: 'Start the Kitematic VM'
},
// Set up the default Kitematic images
{
install: function (callback) {
Meteor.call('reloadDefaultContainers', function (err) {
callback(err);
});
},
pastMessage: 'Started the Kitematic VM',
message: 'Setting up the default Kitematic images...',
subMessage: '(This may take a few minutes)',
futureMessage: 'Set up the default Kitematic images'
}
];
runSetup = function (callback) {
// Run through the Kitematic installation, skipping steps if required.
var currentStep = 0;
Session.set('currentInstallStep', currentStep);
Session.set('numberOfInstallSteps', steps.length);
async.eachSeries(steps, function (step, callback) {
console.log('Performing step ' + currentStep);
step.install(function (err) {
if (err) {
callback(err);
} else {
currentStep += 1;
Session.set('currentInstallStep', currentStep);
callback();
}
});
}, function (err) {
if (err) {
// if any of the steps fail
console.log('Kitematic setup failed at step ' + currentStep);
console.log(err);
Session.set('failedStep', currentStep);
Session.set('failedError', err);
callback(err);
} else {
// Setup Finished
console.log('Setup finished.');
callback();
}
});
};
var installStarted = false; var installStarted = false;
Template.setup_install.rendered = function() { Template.setup_install.rendered = function() {
if(!installStarted) { if(!installStarted) {
installStarted = true; installStarted = true;
runSetup(function (err) { Installer.run(function (err) {
if (err) { if (err) {
console.log('Setup failed.'); console.log('Setup failed.');
console.log(err); console.log(err);
} else { } else {
Installs.insert({}); Installs.insert({version: Installer.CURRENT_VERSION});
startFixInterval();
Router.go('dashboard_apps'); Router.go('dashboard_apps');
} }
}); });
@ -143,7 +15,7 @@ Template.setup_install.rendered = function() {
}; };
Template.setup_install.steps = function () { Template.setup_install.steps = function () {
return steps.map(function (step, index) { return Installer.steps.map(function (step, index) {
step.index = index; step.index = index;
return step; return step;
}); });
@ -154,7 +26,7 @@ Template.setup_install.helpers({
return Session.get('currentInstallStep'); return Session.get('currentInstallStep');
}, },
installComplete: function () { installComplete: function () {
return Session.get('currentInstallStep') === steps.length; return Session.get('currentInstallStep') === Installer.steps.length;
}, },
failedStep: function () { failedStep: function () {
return Session.get('failedStep'); return Session.get('failedStep');

View File

@ -1,8 +1,13 @@
<template name="setup_layout"> <template name="setup_layout">
{{setTitle}} {{setTitle}}
<div class="setup content text-center"> <div class="setup content text-center">
<h2>Welcome to Kitematic</h2> {{#if isUpdating}}
<p>This will set up everything needed to run Kitematic on your Mac.</p> <h2>Welcome Back</h2>
<p>Kitematic needs to update itself to continue.</p>
{{else}}
<h2>Welcome to Kitematic</h2>
<p>This will set up everything needed to run Kitematic on your Mac.</p>
{{/if}}
{{> yield}} {{> yield}}
</div> </div>
</template> </template>

View File

@ -0,0 +1,11 @@
Template.setup_layout.rendered = function () {
Meteor.setInterval(function () {
$('.header .icons a').tooltip();
}, 1000);
};
Template.setup_layout.helpers({
isUpdating: function () {
return Session.get('isUpdating');
}
});

View File

@ -60,7 +60,7 @@ Apps.helpers({
return Images.findOne(this.imageId); return Images.findOne(this.imageId);
}, },
hostUrl: function () { hostUrl: function () {
return this.name + '.dev'; return this.name + '.kite';
}, },
ports: function () { ports: function () {
var app = this; var app = this;
@ -78,7 +78,7 @@ Apps.helpers({
var app = this; var app = this;
var image = Images.findOne(app.imageId); var image = Images.findOne(app.imageId);
if (image && image.meta.app && image.meta.app.webPort) { if (image && image.meta.app && image.meta.app.webPort) {
return 'http://' + app.name + '.dev:' + image.meta.app.webPort; return 'http://' + app.name + '.kite:' + image.meta.app.webPort;
} else if (image && image.meta.app && image.meta.app.webPort === false) { } else if (image && image.meta.app && image.meta.app.webPort === false) {
return null; return null;
} else { } else {
@ -93,14 +93,14 @@ Apps.helpers({
} }
}); });
if (pickedPort) { if (pickedPort) {
return 'http://' + app.name + '.dev:' + pickedPort; return 'http://' + app.name + '.kite:' + pickedPort;
} else { } else {
if (keys.length > 0) { if (keys.length > 0) {
// Picks the first port that's not SSH // Picks the first port that's not SSH
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
var port = parseInt(keys[i].split('/')[0], 10); var port = parseInt(keys[i].split('/')[0], 10);
if (port !== 22) { if (port !== 22) {
return 'http://' + app.name + '.dev:' + port; return 'http://' + app.name + '.kite:' + port;
} }
} }
return null; return null;

View File

@ -1,7 +1,26 @@
Installs = new Meteor.Collection('installs'); Installs = new Meteor.Collection('installs');
schemaInstalls = new SimpleSchema({ schemaInstalls = new SimpleSchema({
createdAt: {
type: Date,
autoValue: function() {
var now = new Date();
if (this.isInsert) {
return now;
} else if (this.isUpsert) {
return {$setOnInsert: now};
} else {
this.unset();
}
},
denyUpdate: true,
label: 'Time of install created'
},
version: {
type: String,
label: 'Installed version',
optional: true
}
}); });
Installs.allow({ Installs.allow({
@ -16,4 +35,4 @@ Installs.allow({
} }
}); });
Installs.attachSchema(schemaInstalls); Installs.attachSchema(schemaInstalls);

View File

@ -70,3 +70,77 @@ Util.copyVolumes = function (directory, appName) {
Util.hasDockerfile = function (directory) { Util.hasDockerfile = function (directory) {
return fs.existsSync(path.join(directory, 'Dockerfile')); return fs.existsSync(path.join(directory, 'Dockerfile'));
}; };
/**
* Compares two software version numbers (e.g. "1.7.1" or "1.2b").
*
* @param {string} v1 The first version to be compared.
* @param {string} v2 The second version to be compared.
* @param {object} [options] Optional flags that affect comparison behavior:
* <ul>
* <li>
* <tt>lexicographical: true</tt> compares each part of the version strings lexicographically instead of
* naturally; this allows suffixes such as "b" or "dev" but will cause "1.10" to be considered smaller than
* "1.2".
* </li>
* <li>
* <tt>zeroExtend: true</tt> changes the result if one version string has less parts than the other. In
* this case the shorter string will be padded with "zero" parts instead of being considered smaller.
* </li>
* </ul>
* @returns {number|NaN}
* <ul>
* <li>0 if the versions are equal</li>
* <li>a negative integer iff v1 < v2</li>
* <li>a positive integer iff v1 > v2</li>
* <li>NaN if either version string is in the wrong format</li>
* </ul>
*
*/
Util.compareVersions = function (v1, v2, options) {
var lexicographical = options && options.lexicographical,
zeroExtend = options && options.zeroExtend,
v1parts = v1.split('.'),
v2parts = v2.split('.');
function isValidPart(x) {
return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
}
if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
return NaN;
}
if (zeroExtend) {
while (v1parts.length < v2parts.length) v1parts.push('0');
while (v2parts.length < v1parts.length) v2parts.push('0');
}
if (!lexicographical) {
v1parts = v1parts.map(Number);
v2parts = v2parts.map(Number);
}
for (var i = 0; i < v1parts.length; ++i) {
if (v2parts.length == i) {
return 1;
}
if (v1parts[i] == v2parts[i]) {
continue;
}
else if (v1parts[i] > v2parts[i]) {
return 1;
}
else {
return -1;
}
}
if (v1parts.length != v2parts.length) {
return -1;
}
return 0;
};

View File

@ -14,7 +14,7 @@ Apps.restart = function (app, callback) {
}).run(); }).run();
callback(null); callback(null);
// Use dig to refresh the DNS // Use dig to refresh the DNS
exec('/usr/bin/dig dig ' + app.name + '.dev @172.17.42.1 ', function() {}); exec('/usr/bin/dig dig ' + app.name + '.kite @172.17.42.1 ', function() {});
} else { } else {
callback(null); callback(null);
} }

View File

@ -1,7 +1,7 @@
Dockerode = Meteor.require('dockerode'); Dockerode = Meteor.require('dockerode');
var DOCKER_HOST='192.168.59.103'; var DOCKER_HOST='192.168.60.103';
docker = new Dockerode({host: '192.168.59.103', port: '2375'}); docker = new Dockerode({host: DOCKER_HOST, port: '2375'});
Docker = {}; Docker = {};
@ -72,7 +72,7 @@ Docker.runContainer = function (app, image, callback) {
if (err) { callback(err, null); return; } if (err) { callback(err, null); return; }
console.log('Started container: ' + container.id); console.log('Started container: ' + container.id);
// Use dig to refresh the DNS // Use dig to refresh the DNS
exec('/usr/bin/dig dig ' + app.name + '.dev @172.17.42.1 ', function() {}); exec('/usr/bin/dig dig ' + app.name + '.kite @172.17.42.1 ', function() {});
callback(null, container); callback(null, container);
}); });
}); });

View File

@ -1,45 +1,39 @@
#!/bin/sh #!/bin/sh
# Lookup the Kitematic VM resolver for .dev domains # This script must be run as root and sets up Mac OS X to route all .kite domains to the virtual box VM with the name
# 'boot2docker-vm'. It does the following:
# 1) Adds a file under /etc/resolver/kite
# 2) Sets up a LaunchAgent for adding entries to the route table to route all requests to the Docker subnet (172.17.0.0/16)
# And expects the $IFNAME variable to contain the interface on which to send traffic to the boot2docker VM.
mkdir -p /etc/resolver mkdir -p /etc/resolver
echo "nameserver 172.17.42.1" > /etc/resolver/dev echo "nameserver 172.17.42.1" > /etc/resolver/kite
DIR=$(dirname "$0") DIR=$(dirname "$0")
USER=`w -h | sort -u -t' ' -k1,1 | awk '{print $1}'` USER=`w -h | sort -u -t' ' -k1,1 | awk '{print $1}'`
/bin/rm -rf /Library/LaunchAgents/com.kitematic.route.plist /bin/rm -rf /Library/LaunchAgents/com.kitematic.route.plist
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
echo '<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version=\"1.0\">
<plist version="1.0">
<dict> <dict>
<key>Label</key> <key>Label</key>
<string>com.kitematic.route</string> <string>com.kitematic.route</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/sbin/route</string> <string>bash</string>
<string>-n</string> <string>-c</string>
<string>add</string> <string>/usr/sbin/scutil -w State:/Network/Interface/$IFNAME/IPv4;/sbin/route -n add -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY</string>
<string>172.17.0.0/16</string>
<string>192.168.59.103</string>
</array> </array>
<key>KeepAlive</key> <key>KeepAlive</key>
<false/> <false/>
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>
<key>ServiceIPC</key>
<false/>
<key>UserName</key>
<string>root</string>
<key>LaunchOnlyOnce</key> <key>LaunchOnlyOnce</key>
<true/> <true/>
</dict> </dict>
</plist>' > /Library/LaunchAgents/com.kitematic.route.plist </plist>" > /Library/LaunchAgents/com.kitematic.route.plist
sudo -u $USER $DIR/boot2docker init
VBoxManage modifyvm boot2docker-vm --nic2 hostonly --nictype2 virtio --cableconnected2 on --hostonlyadapter2 vboxnet0
VBoxManage dhcpserver add --netname=vboxnet0 --ip=192.168.59.99 --netmask=255.255.255.0 --lowerip=192.168.59.103 --upperip=192.168.59.103
# Add entries to routing table for Kitematic VM # Add entries to routing table for Kitematic VM
/sbin/route delete 172.17.0.0/16 192.168.59.103 /sbin/route delete -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY > /dev/null 2>&1 || true
/sbin/route -n add 172.17.0.0/16 192.168.59.103 /sbin/route -n add -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY

View File

@ -1,6 +1,6 @@
user=root user=root
{{ range $index, $value := $ }} {{ range $index, $value := $ }}
{{ with $address := index $value.Addresses 0 }} {{ with $address := index $value.Addresses 0 }}
address=/{{$value.Name}}.dev/{{$address.IP}} address=/{{$value.Name}}.kite/{{$address.IP}}
{{ end }} {{ end }}
{{ end }} {{ end }}

View File

@ -15,6 +15,7 @@ pushd cache
if [ ! -f $BASE_IMAGE_VERSION_FILE ]; then if [ ! -f $BASE_IMAGE_VERSION_FILE ]; then
cecho "-----> Downloading Kitematic base images..." $purple cecho "-----> Downloading Kitematic base images..." $purple
curl -L --progress-bar -o $BASE_IMAGE_VERSION_FILE https://s3.amazonaws.com/kite-installer/$BASE_IMAGE_VERSION_FILE curl -L --progress-bar -o $BASE_IMAGE_VERSION_FILE https://s3.amazonaws.com/kite-installer/$BASE_IMAGE_VERSION_FILE
cp $BASE_IMAGE_VERSION_FILE ../resources/$BASE_IMAGE_FILE
fi fi
if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then
@ -60,11 +61,6 @@ if [ ! -f $COCOASUDO_FILE ]; then
chmod +x $COCOASUDO_FILE chmod +x $COCOASUDO_FILE
fi fi
if [ ! -f $BASE_IMAGE_FILE ]; then
cp ../cache/$BASE_IMAGE_VERSION_FILE $BASE_IMAGE_FILE
fi
cp ../cache/$BOOT2DOCKER_CLI_VERSION_FILE $BOOT2DOCKER_CLI_FILE cp ../cache/$BOOT2DOCKER_CLI_VERSION_FILE $BOOT2DOCKER_CLI_FILE
chmod +x $BOOT2DOCKER_CLI_FILE chmod +x $BOOT2DOCKER_CLI_FILE

View File

@ -1,4 +1,4 @@
BASE_IMAGE_VERSION=0.0.1 BASE_IMAGE_VERSION=0.0.2
BASE_IMAGE_VERSION_FILE=base-images-$BASE_IMAGE_VERSION.tar.gz BASE_IMAGE_VERSION_FILE=base-images-$BASE_IMAGE_VERSION.tar.gz
BASE_IMAGE_FILE=base-images.tar.gz BASE_IMAGE_FILE=base-images.tar.gz