diff --git a/index.js b/index.js index 3a3d3a4989..ba3a23f38f 100644 --- a/index.js +++ b/index.js @@ -43,10 +43,7 @@ var start = function (callback) { freeport(function(err, mongoPort) { console.log('MongoDB: ' + mongoPort); console.log('webPort: ' + webPort); - child_process.exec('kill $(ps aux -e | grep PURPOSE=KITEMATIC | awk \'{print $2}\')', function (error, stdout, stderr) { - console.log(error); - console.log(stdout); - console.log(stderr); + child_process.exec('kill $(ps aux -e | grep PURPOSE=KITEMATIC | awk \'{print $2}\') && rm ' + path.join(dataPath, 'mongod.lock'), function (error, stdout, stderr) { var mongoChild = child_process.spawn(path.join(process.cwd(), 'resources', 'mongod'), ['--bind_ip', '127.0.0.1', '--dbpath', dataPath, '--port', mongoPort, '--unixSocketPrefix', dataPath], { env: { PURPOSE: 'KITEMATIC' @@ -54,7 +51,12 @@ var start = function (callback) { }); var started = false; mongoChild.stdout.setEncoding('utf8'); + mongoChild.stderr.setEncoding('utf8'); + mongoChild.stderr.on('data', function (data) { + console.log(data); + }); mongoChild.stdout.on('data', function (data) { + console.log(data); if (data.indexOf('waiting for connections on port ' + mongoPort)) { if (!started) { started = true; diff --git a/meteor/.jshintrc b/meteor/.jshintrc index e2c8f682e4..ab5b799ac1 100755 --- a/meteor/.jshintrc +++ b/meteor/.jshintrc @@ -128,7 +128,7 @@ // Packages "Fiber": true, "moment": true, - "Docker": true, + "Dockerode": true, "byline": true, "fs": true, "zlib": true, @@ -142,6 +142,9 @@ "chokidar": true, "docker": true, "async": true, + "child_process": true, + "convert": true, + "Convert": true, // Collections "SimpleSchema": false, @@ -161,8 +164,13 @@ "SetupController": true, // Server and Client + "Docker": true, + "Util": true, + "Boot2Docker": true, + "Installer": true, + "VirtualBox": true, + "boot2dockerexec": true, - "getBinDir": true, "getBoot2DockerIp": true, "getBoot2DockerState": true, "getBoot2DockerDiskUsage": true, @@ -170,7 +178,6 @@ "getBoot2DockerInfo": true, "boot2DockerVMExists": true, "eraseBoot2DockerVMFiles": true, - "getHomePath": true, "initBoot2Docker": true, "isVirtualBoxInstalled": true, "upgradeBoot2Docker": true, @@ -191,46 +198,17 @@ "fixInterval": true, "stopFixInterval": true, "runSetup": true, - "removeBindFolder": true, "removeAppWatcher": true, "addAppWatcher": true, "resolveWatchers": true, - "recoverApps": true, - "restartApp": true, - "deleteApp": true, - "deleteFolder": true, - "loadKiteVolumes": true, - "getAppLogs": true, - "hasDockerfile": true, - "runContainer": true, - "runContainerSync": true, - "restartContainer": true, - "restartContainerSync": true, - "createTarFile": true, - "createTarFileSync": true, - "getImageData": true, - "getImageDataSync": true, - "removeImage": true, - "removeImageSync": true, - "deleteImage": true, "checkDefaultImages": true, "resolveDefaultImages": true, "checkDefaultContainers": true, "resolveDefaultContainers": true, "killAndRemoveContainers": true, - "deleteImageSync": true, "upContainers": true, "reloadDefaultContainers": true, "removeImages": true, - "pullImageFromDockerfile": true, - "buildImage": true, - "getImageMetaData": true, - "getImageJSON": true, - "rebuildImage": true, - "saveImageFolderSync": true, - "rebuildImageSync": true, - "saveImageFolder": true, - "copyFolder": true, // Forms "showFormErrors": true, @@ -239,11 +217,6 @@ "FormSchema": true, "showFormSuccess": true, "resetForm": true, - "removeContainer": true, - "removeContainerSync": true, - "deleteAppSync": true, - "getContainerData": true, - "getContainerDataSync": true, // Testing "require": false, diff --git a/meteor/client/lib/boot2docker.js b/meteor/client/lib/boot2docker.js index 8e939a2dc3..589b85b67d 100644 --- a/meteor/client/lib/boot2docker.js +++ b/meteor/client/lib/boot2docker.js @@ -1,14 +1,54 @@ var exec = require('exec'); var path = require('path'); -boot2dockerexec = function (command, callback) { - exec(path.join(getBinDir(), 'boot2docker') + ' ' + command, function(err, stdout) { - callback(err, stdout); +Boot2Docker = {}; + +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) { - boot2dockerexec('ip', function (err, stdout) { +Boot2Docker.exists = function (callback) { + 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) { callback(err, null); } else { @@ -17,12 +57,54 @@ getBoot2DockerIp = function (callback) { }); }; -getBoot2DockerState = function (callback) { - boot2dockerexec(' info', function (err, stdout) { - if (err) { - callback(err, null); +Boot2Docker.setIp = function (ifname, ip, callback) { + this.exec('ssh "sudo ifconfig ' + ifname + ' ' + ip + ' netmask 255.255.255.0"', function (err, stdout) { + callback(err); + }); +}; + +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; } + 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 { var info = JSON.parse(stdout); callback(null, info.State); @@ -32,8 +114,8 @@ getBoot2DockerState = function (callback) { }); }; -getBoot2DockerDiskUsage = function (callback) { - boot2dockerexec('ssh "df"', function (err, stdout) { +Boot2Docker.diskUsage = function (callback) { + this.exec('ssh "df"', function (err, stdout) { if (err) { callback(err, null); return; @@ -61,8 +143,8 @@ getBoot2DockerDiskUsage = function (callback) { }); }; -getBoot2DockerMemoryUsage = function (callback) { - boot2dockerexec('ssh "free -m"', function (err, stdout) { +Boot2Docker.memoryUsage = function (callback) { + this.exec('ssh "free -m"', function (err, stdout) { if (err) { callback(err, null); return; @@ -92,178 +174,105 @@ getBoot2DockerMemoryUsage = function (callback) { }); }; -getBoot2DockerInfo = function (callback) { - 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) { - getBoot2DockerState(function (err, state) { +Boot2Docker.stats = function (callback) { + var self = this; + self.state(function (err, state) { + if (err) { callback(err, null); return; } + if (state === 'poweroff') { + callback(null, {state: state}); + return; + } + self.memoryUsage(function (err, mem) { if (err) { - callback(err, null); + callback(null, {state: state}); return; } - if (state === 'poweroff') { - 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(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); + self.diskUsage(function (err, disk) { if (err) { - if (err.indexOf('exit status 1') !== -1) { - eraseBoot2DockerVMFiles(function () { - boot2dockerexec('init', function (err) { - callback(err); - }); - }); - } else { - callback(err); - } - } else { - callback(); + callback(null, {state: state, memory: mem}); + return; } + 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 { - callback(new Error('initBoot2Docker called but VirtualBox isn\'t installed.')); + callback(null, stdout); } }); }; -upgradeBoot2Docker = function (callback) { - boot2dockerexec('upgrade', function (err, stdout) { - console.log(stdout); +Boot2Docker.version = function (callback) { + this.exec('version', function (err, stdout, stderr) { + if (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]); + } + }); +}; + +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) { callback(err); }); }; -installBoot2DockerAddons = function (callback) { - exec('/bin/cat ' + path.join(getBinDir(), 'kite-binaries.tar.gz') + ' | ' + path.join(getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) { - console.log(stdout); - 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) { - isVirtualBoxInstalled(function (err, installed) { - if (err) { - 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) { +Boot2Docker.check = function (callback) { + var self = this; + self.exists(function (err, exists) { if (err) { callback(err); return; } else { - getBoot2DockerState(function (err, state) { + self.state(function (err, state) { if (state !== 'running') { callback('boot2docker not running'); } else { - callback(); + self.correct(function (err) { + callback(err); + }); } }); } }); }; -// Make sure the VM exists, is up and is running. -resolveBoot2DockerVM = function (callback) { - boot2DockerVMExists(function (err, exists) { - +Boot2Docker.resolve = function (callback) { + var self = this; + self.exists(function (err, exists) { // If somehow the boot2docker VM doesn't exist anymor then re-create it. if (!exists) { - initBoot2Docker(function () { - startBoot2Docker(function (err) { + self.init(function () { + self.start(function (err) { callback(err); }); }); } else { - // If it exists but it's not running.. restart it. - getBoot2DockerState(function (err, state) { + self.state(function (err, state) { if (state !== 'running') { - startBoot2Docker(function (err) { + self.start(function (err) { callback(err); }); } else { diff --git a/meteor/client/lib/installer.js b/meteor/client/lib/installer.js new file mode 100644 index 0000000000..459ed06f7b --- /dev/null +++ b/meteor/client/lib/installer.js @@ -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(); + } + }); +}; diff --git a/meteor/client/lib/requires.js b/meteor/client/lib/requires.js new file mode 100644 index 0000000000..2b261be406 --- /dev/null +++ b/meteor/client/lib/requires.js @@ -0,0 +1,2 @@ +path = require('path'); +fs = require('fs'); diff --git a/meteor/client/lib/router.js b/meteor/client/lib/router.js index 0e9f2ecb6e..6e8168efd6 100755 --- a/meteor/client/lib/router.js +++ b/meteor/client/lib/router.js @@ -48,12 +48,16 @@ Router.map(function () { controller: 'SetupController', action: function () { if (this.ready()) { - var install = Installs.findOne(); - if (!install) { - console.log('No installs detected, running installer again.'); - this.redirect('/setup/intro'); + if (!Installer.isUpToDate()) { + if (!Installs.findOne()) { + console.log('No installs detected, running installer again.'); + 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 { - startFixInterval(); this.redirect('/apps'); } } diff --git a/meteor/client/lib/sync.js b/meteor/client/lib/sync.js index 4dbbcace5d..cd2468ef2d 100644 --- a/meteor/client/lib/sync.js +++ b/meteor/client/lib/sync.js @@ -15,7 +15,7 @@ removeAppWatcher = function (id) { addAppWatcher = function (app) { removeAppWatcher(app._id); - var appPath = path.join(path.join(getHomePath(), 'Kitematic'), app.name); + var appPath = path.join(path.join(Util.getHomePath(), 'Kitematic'), app.name); var vmDir = path.join('/var/lib/docker/binds', app.name); var vmPath = 'ssh://docker@localhost:2022/' + vmDir; var watcher = chokidar.watch(appPath, {ignored: /.*\.DS_Store/}); @@ -30,7 +30,7 @@ addAppWatcher = function (app) { syncing = true; var errorPattern = /The\sfile\s(.*)\son\shost/g; var archiveErrorPattern = /Archive\s(.*)\son\shost\s.*\sshould\sbe\sDELETED/g; - var cmd = path.join(getBinDir(), 'unison'); + var cmd = path.join(Util.getBinDir(), 'unison'); var args = [ cmd, vmPath, @@ -46,7 +46,7 @@ addAppWatcher = function (app) { 'Name\ {*.tmp,*.unison,*.swp,*.pyc,.DS_Store}', '-auto', '-sshargs', - '-o\ UserKnownHostsFile=/dev/null\ -o\ StrictHostKeyChecking=no\ -o PreferredAuthentications=publickey\ -i\ ' + path.join(getHomePath(), '.ssh/id_boot2docker') + '-o\ UserKnownHostsFile=/dev/null\ -o\ StrictHostKeyChecking=no\ -o PreferredAuthentications=publickey\ -i\ ' + path.join(Util.getHomePath(), '.ssh/id_boot2docker') ]; if (!fs.existsSync(appPath)) { @@ -70,12 +70,12 @@ addAppWatcher = function (app) { if (err.indexOf('The archive file is missing on some hosts') !== -1) { var results = archiveErrorPattern.exec(err); var location = results[1].replace(' ', '\\ '); - var fullLocation = path.join(getHomePath(), 'Library/Application\\ Support/Unison', location); + var fullLocation = path.join(Util.getHomePath(), 'Library/Application\\ Support/Unison', location); var cmd = '/bin/rm -rf ' + fullLocation; exec(cmd, function () {}); } } catch (e) { - // console.error(e); + //console.error(e); } syncing = false; if (willSyncAgain) { @@ -118,4 +118,4 @@ resolveWatchers = function (callback) { }); callback(); -}; \ No newline at end of file +}; diff --git a/meteor/client/lib/utilities.js b/meteor/client/lib/utilities.js index c3fe268798..3315ad4040 100755 --- a/meteor/client/lib/utilities.js +++ b/meteor/client/lib/utilities.js @@ -1,17 +1,3 @@ -var path = require('path'); - -getBinDir = function () { - if (process.env.NODE_ENV === 'development') { - return path.join(path.join(process.env.PWD, '..'), 'resources'); - } else { - return path.join(process.cwd(), 'resources'); - } -}; - -getHomePath = function () { - return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; -}; - showFormErrors = function ($form, errors) { for (var name in errors) { if (errors.hasOwnProperty(name)) { @@ -31,7 +17,7 @@ clearFormErrors = function ($form) { $form.find('.form-group.has-error .help-block.error').remove(); $form.find('.form-group.has-error').removeClass('has-error'); }; - + resetForm = function ($form) { $form.find('input').val(''); }; @@ -41,4 +27,4 @@ trackLink = function (trackLabel) { console.log(trackLabel); ga('send', 'event', 'link', 'click', trackLabel); } -}; \ No newline at end of file +}; diff --git a/meteor/client/lib/virtualbox.js b/meteor/client/lib/virtualbox.js index 6d362492a9..4368f5c72f 100644 --- a/meteor/client/lib/virtualbox.js +++ b/meteor/client/lib/virtualbox.js @@ -1,55 +1,158 @@ var fs = require('fs'); -var child_process = require('child_process'); +var exec = require('exec'); var path = require('path'); -isVirtualBoxInstalled = function (callback) { - fs.exists('/usr/bin/VBoxManage', function (exists) { - callback(null, exists); +VirtualBox = {}; + +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) { - fs.readFile('/etc/resolver/dev', { - encoding: 'utf8' - }, function (err, data) { +VirtualBox.install = function (callback) { + // -W waits for the process to close before finishing. + exec('open -W ' + path.join(Util.getBinDir(), this.INSTALLER_FILENAME), function (error, stdout, stderr) { + 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) { - callback(err, false); - } else { - if (data.indexOf('nameserver 172.17.42.1') !== -1) { - callback(null, true); - } else { - callback(null, false); + callback(err); + return; + } + // Output is x.x.xryyyyyy + var match = stdout.match(/(\d+\.\d+\.\d+).*/); + 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); }); }; -setupResolver = function (callback) { - var installFile = path.join(getBinDir(), 'install'); - var cocoaSudo = path.join(getBinDir(), 'cocoasudo'); - var execCommand = cocoaSudo + ' --prompt="Kitematic Setup wants to make changes. Type your password to allow this." ' + installFile; - child_process.exec(execCommand, function (error, stdout, stderr) { - console.log(stdout); - if (error) { - console.log(error); - callback(error); +VirtualBox.hostOnlyAdapters = function (vm, callback) { + this.exec('showvminfo ' + vm + ' --machinereadable', function (err, stdout, stderr) { + if (err) { + callback(err); return; } - console.log('Virtualbox Installation & Resolver config complete.'); - callback(); - }); -}; - -setupVirtualBox = function (callback) { - child_process.exec('open -W ' + path.join(getBinDir(), 'virtualbox-4.3.12.pkg'), function (error, stdout, stderr) { - console.log(stdout); - if (error) { - console.log(error); - callback(error); - 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); } - console.log('Virtualbox Installation running.'); - callback(); }); }; +VirtualBox.hostOnlyAdapter = function (callback) { + var self = this; + self.hostOnlyIfs(function (err, ifs) { + var iface = _.findWhere(_.toArray(ifs), {IPAddress: VirtualBox.HOSTONLY_HOSTIP}); + if (!iface) { + self.exec('hostonlyif create', function (err, stdout, stderr) { + var match = stdout.match(/Interface '(vboxnet\d+)' was successfully created/); + if (!match) { + callback('Could not parse output of hostonlyif create'); + 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); + } + }); +}; + +VirtualBox.addCustomHostAdapter = function (vm, callback) { + var self = this; + self.hostOnlyAdapter(function (err, ifname) { + if (err) { callback(err); return; } + self.exec('modifyvm ' + vm + ' --nic3 hostonly --nictype3 virtio --cableconnected3 on --hostonlyadapter3 ' + ifname, function (err, stdout, stderr) { + callback(err, ifname); + }); + }); +}; + +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); + }); + }); +}; diff --git a/meteor/client/main.js b/meteor/client/main.js index 7abe7d59a1..429201d295 100755 --- a/meteor/client/main.js +++ b/meteor/client/main.js @@ -68,24 +68,11 @@ Meteor.call('getDockerHost', function (err, 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) { - checkBoot2DockerVM(function (err) { + Boot2Docker.check(function (err) { if (err) { Session.set('available', false); - resolveBoot2DockerVM(function (err) { + Boot2Docker.resolve(function (err) { if (err) { callback(err); } else { @@ -138,28 +125,41 @@ fixDefaultContainers = function (callback) { }; 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); -fixInterval = null; -startFixInterval = function () { - stopFixInterval(); - fixInterval = Meteor.setInterval(function () { +Meteor.setInterval(function () { + if (Installer.isUpToDate()) { resolveWatchers(function () {}); - fixBoot2DockerVM(function (err) { - if (err) { console.log(err); return; } - // Meteor.call('recoverApps'); - fixDefaultImages(function (err) { + if (!Session.get('boot2dockerOff')) { + fixBoot2DockerVM(function (err) { if (err) { console.log(err); return; } - fixDefaultContainers(function (err) { - if (err) { console.log(err); } + Meteor.call('recoverApps'); + 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; -}; \ No newline at end of file diff --git a/meteor/client/views/dashboard/apps/dashboard-single-app.js b/meteor/client/views/dashboard/apps/dashboard-single-app.js index acbd198e79..5d32c4818f 100755 --- a/meteor/client/views/dashboard/apps/dashboard-single-app.js +++ b/meteor/client/views/dashboard/apps/dashboard-single-app.js @@ -21,8 +21,8 @@ Template.dashboard_single_app.events({ }, 'click .btn-terminal': function () { var app = this; - var cmd = path.join(getBinDir(), 'boot2docker') + ' ssh -t "sudo docker-enter ' + app.docker.Id + '"'; - var terminalCmd = path.join(getBinDir(), 'terminal') + ' ' + cmd; + var cmd = path.join(Util.getBinDir(), 'boot2docker') + ' ssh -t "sudo docker-enter ' + app.docker.Id + '"'; + var terminalCmd = path.join(Util.getBinDir(), 'terminal') + ' ' + cmd; var exec = require('child_process').exec; console.log(terminalCmd); exec(terminalCmd, function (err, stdout) { diff --git a/meteor/client/views/dashboard/settings/dashboard-settings.js b/meteor/client/views/dashboard/settings/dashboard-settings.js index cf12f91c27..1a2ef4b7bd 100644 --- a/meteor/client/views/dashboard/settings/dashboard-settings.js +++ b/meteor/client/views/dashboard/settings/dashboard-settings.js @@ -3,18 +3,22 @@ Template.dashboard_settings.events({ var $btn = $(e.currentTarget); $btn.html('Starting Boot2Docker...'); $btn.attr("disabled", "disabled"); - startFixInterval(); - startBoot2Docker(function (err) { - if (err) { console.error(err); } + Session.set('boot2dockerOff', false); + Boot2Docker.start(function (err) { + if (err) { + console.log(err); + } }); }, 'click .btn-stop-boot2docker': function (e) { var $btn = $(e.currentTarget); $btn.html('Stopping Boot2Docker...'); $btn.attr("disabled", "disabled"); - stopFixInterval(); - stopBoot2Docker(function (err) { - if (err) { console.error(err); } + Session.set('boot2dockerOff', true); + Boot2Docker.stop(function (err) { + if (err) { + console.log(err); + } }); } }); diff --git a/meteor/client/views/dashboard/setup/setup-install.js b/meteor/client/views/dashboard/setup/setup-install.js index 004ac755cf..d5774fbbd7 100644 --- a/meteor/client/views/dashboard/setup/setup-install.js +++ b/meteor/client/views/dashboard/setup/setup-install.js @@ -1,142 +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) { - var installedYet = false; - 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; Template.setup_install.rendered = function() { if(!installStarted) { installStarted = true; - runSetup(function (err) { + Installer.run(function (err) { if (err) { console.log('Setup failed.'); console.log(err); } else { - Installs.insert({}); - startFixInterval(); + Installs.insert({version: Installer.CURRENT_VERSION}); Router.go('dashboard_apps'); } }); @@ -144,7 +15,7 @@ Template.setup_install.rendered = function() { }; Template.setup_install.steps = function () { - return steps.map(function (step, index) { + return Installer.steps.map(function (step, index) { step.index = index; return step; }); @@ -155,7 +26,7 @@ Template.setup_install.helpers({ return Session.get('currentInstallStep'); }, installComplete: function () { - return Session.get('currentInstallStep') === steps.length; + return Session.get('currentInstallStep') === Installer.steps.length; }, failedStep: function () { return Session.get('failedStep'); diff --git a/meteor/client/views/dashboard/setup/setup-layout.html b/meteor/client/views/dashboard/setup/setup-layout.html index 12c0f8682b..ce3765beb4 100644 --- a/meteor/client/views/dashboard/setup/setup-layout.html +++ b/meteor/client/views/dashboard/setup/setup-layout.html @@ -1,8 +1,13 @@ diff --git a/meteor/client/views/dashboard/setup/setup-layout.js b/meteor/client/views/dashboard/setup/setup-layout.js new file mode 100644 index 0000000000..2d99b3bc4b --- /dev/null +++ b/meteor/client/views/dashboard/setup/setup-layout.js @@ -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'); + } +}); diff --git a/meteor/collections/apps.js b/meteor/collections/apps.js index 3b0ff8b147..85cf5c5266 100755 --- a/meteor/collections/apps.js +++ b/meteor/collections/apps.js @@ -60,7 +60,7 @@ Apps.helpers({ return Images.findOne(this.imageId); }, hostUrl: function () { - return this.name + '.dev'; + return this.name + '.kite'; }, ports: function () { var app = this; @@ -78,7 +78,7 @@ Apps.helpers({ var app = this; var image = Images.findOne(app.imageId); 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) { return null; } else { @@ -93,14 +93,14 @@ Apps.helpers({ } }); if (pickedPort) { - return 'http://' + app.name + '.dev:' + pickedPort; + return 'http://' + app.name + '.kite:' + pickedPort; } else { if (keys.length > 0) { // Picks the first port that's not SSH for (var i = 0; i < keys.length; i++) { var port = parseInt(keys[i].split('/')[0], 10); if (port !== 22) { - return 'http://' + app.name + '.dev:' + port; + return 'http://' + app.name + '.kite:' + port; } } return null; @@ -116,3 +116,41 @@ Apps.helpers({ }); Apps.attachSchema(schemaApps); + +Apps.after.insert(function (userId, app) { + // Give app an unique environment variable + var appId = this._id; + Apps.update(appId, { + $set: { + 'config.APP_ID': appId + } + }); + var image = Images.findOne(app.imageId); + Util.copyVolumes(image.path, app.name); + app = Apps.findOne(appId); + Docker.removeBindFolder(app.name, function (err) { + if (err) { + console.error(err); + } + Fiber(function () { + Meteor.call('runApp', app, function (err) { + if (err) { throw err; } + }); + }).run(); + }); +}); + +Apps.after.remove(function (userId, app) { + if (app.docker) { + try { + Docker.removeContainerSync(app.docker.Id); + } catch (e) { + console.error(e); + } + } + var appPath = path.join(KITE_PATH, app.name); + Util.deleteFolder(appPath); + Docker.removeBindFolder(app.name, function () { + console.log('Deleted Kite ' + app.name + ' directory.'); + }); +}); diff --git a/meteor/collections/images.js b/meteor/collections/images.js index 15055ad021..5a4de5b4f8 100755 --- a/meteor/collections/images.js +++ b/meteor/collections/images.js @@ -91,3 +91,43 @@ Images.allow({ }); Images.attachSchema(schemaImages); + +Images.after.insert(function (userId, image) { + var imageId = this._id; + var imagePath = path.join(KITE_IMAGES_PATH, imageId); + Images.update(imageId, { + $set: { + path: imagePath + } + }); + if (image.meta.logo) { + Images.update(imageId, { + $set: { + logoPath: path.join(imagePath, image.meta.logo) + } + }); + } + image = Images.findOne(imageId); + Images.saveFolderSync(image.originPath, imageId); + Images.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), imageId, function (err) { + if (err) { throw err; } + Images.build(image, function (err) { + if (err) { console.error(err); } + }); + }); +}); + +Images.after.remove(function (userId, image) { + if (image.docker) { + try { + Docker.removeImageSync(image.docker.Id); + } catch (e) { + console.error(e); + } + } + try { + Util.deleteFolder(image.path); + } catch (e) { + console.error(e); + } +}); diff --git a/meteor/collections/installs.js b/meteor/collections/installs.js index 6f0a492ce7..314b538050 100644 --- a/meteor/collections/installs.js +++ b/meteor/collections/installs.js @@ -1,7 +1,26 @@ Installs = new Meteor.Collection('installs'); 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({ @@ -16,4 +35,4 @@ Installs.allow({ } }); -Installs.attachSchema(schemaInstalls); +Installs.attachSchema(schemaInstalls); \ No newline at end of file diff --git a/meteor/lib/utilities.js b/meteor/lib/utilities.js new file mode 100644 index 0000000000..363ab245e7 --- /dev/null +++ b/meteor/lib/utilities.js @@ -0,0 +1,146 @@ +Util = {}; + +Util.getHomePath = function () { + return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; +}; + +Util.getBinDir = function () { + if (process.env.NODE_ENV === 'development') { + return path.join(path.join(process.env.PWD, '..'), 'resources'); + } else { + if (Meteor.isClient) { + return path.join(process.cwd(), 'resources'); + } else { + return path.join(process.cwd(), '../../../resources'); + } + } +}; + +Util.deleteFolder = function (directory) { + if (fs.existsSync(directory)) { + fs.readdirSync(directory).forEach(function (file) { + var curDirectory = directory + '/' + file; + if (fs.lstatSync(curDirectory).isDirectory()) { + // Recurse + Util.deleteFolder(curDirectory); + } else { + // Delete File + try { + fs.unlinkSync(curDirectory); + } catch (e) { + console.error(e); + } + } + }); + fs.rmdirSync(directory); + } +}; + +Util.copyFolder = function (src, dest) { + var exists = fs.existsSync(src); + var stats = exists && fs.statSync(src); + var isDirectory = exists && stats.isDirectory(); + if (exists && isDirectory) { + try { + fs.mkdirSync(dest); + } catch (e) { + console.error(e); + } + fs.readdirSync(src).forEach(function (childItemName) { + Util.copyFolder(path.join(src, childItemName), path.join(dest, childItemName)); + }); + } else { + try { + fs.linkSync(src, dest); + } catch (e) { + console.error(e); + } + } +}; + +Util.copyVolumes = function (directory, appName) { + var KITE_VOLUMES_PATH = path.join(directory, 'volumes'); + if (fs.existsSync(KITE_VOLUMES_PATH)) { + var destinationPath = path.join(KITE_PATH, appName); + Util.copyFolder(KITE_VOLUMES_PATH, destinationPath); + console.log('Copied volumes for: ' + appName); + } +}; + +Util.hasDockerfile = function (directory) { + 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: + * + * @returns {number|NaN} + * + * + */ +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; +}; + diff --git a/meteor/server/apps.js b/meteor/server/apps.js index af9383afa3..a8e7c8dce7 100755 --- a/meteor/server/apps.js +++ b/meteor/server/apps.js @@ -1,10 +1,55 @@ -removeBindFolder = function (name, callback) { - exec(path.join(getBinDir(), 'boot2docker') + ' ssh "sudo rm -rf /var/lib/docker/binds/' + name + '"', function(err, stdout) { - callback(err, stdout); - }); +Apps.restart = function (app, callback) { + if (app.docker && app.docker.Id) { + try { + Docker.restartContainerSync(app.docker.Id); + } catch (e) { + console.error(e); + } + var containerData = Docker.getContainerDataSync(app.docker.Id); + Fiber(function () { + Apps.update(app._id, {$set: { + status: 'READY', + docker: containerData + }}); + }).run(); + callback(null); + // Use dig to refresh the DNS + exec('/usr/bin/dig dig ' + app.name + '.dev @172.17.42.1 ', function() {}); + } else { + callback(null); + } }; -recoverApps = function (callback) { +Apps.logs = function (app) { + if (app.docker && app.docker.Id) { + var container = docker.getContainer(app.docker.Id); + container.logs({follow: false, stdout: true, stderr: true, timestamps: true, tail: 300}, function (err, response) { + if (err) { throw err; } + Fiber(function () { + Apps.update(app._id, { + $set: { + logs: [] + } + }); + }).run(); + var logs = []; + response.setEncoding('utf8'); + response.on('data', function (line) { + logs.push(convert.toHtml(line.slice(8))); + Fiber(function () { + Apps.update(app._id, { + $set: { + logs: logs + } + }); + }).run(); + }); + response.on('end', function () {}); + }); + } +}; + +Apps.recover = function (callback) { var apps = Apps.find({}).fetch(); _.each(apps, function (app) { // Update the app with the latest container info @@ -17,7 +62,7 @@ recoverApps = function (callback) { console.log('restarting: ' + app.name); console.log(app.docker.Id); Fiber(function () { - restartApp(app, function (err) { + Apps.restart(app, function (err) { if (err) { console.error(err); } }); }).run(); @@ -29,7 +74,8 @@ recoverApps = function (callback) { Meteor.methods({ recoverApps: function () { - return Meteor._wrapAsync(recoverApps)(); + this.unblock(); + return Meteor._wrapAsync(Apps.recover)(); }, configVar: function (appId, configVars) { this.unblock(); @@ -48,64 +94,38 @@ Meteor.methods({ if (!app) { throw new Meteor.Error(403, 'No app found with this ID'); } - deleteApp(app, function (err) { - if (err) { console.error(err); } - var appPath = path.join(KITE_PATH, app.name); - deleteFolder(appPath); - removeBindFolder(app.name, function () { - console.log('Deleted Kite ' + app.name + ' directory.'); - Fiber(function () { - Apps.remove({_id: app._id}); - }).run(); - }); - }); + Apps.remove({_id: app._id}); }, createApp: function (formData) { + this.unblock(); var validationResult = formValidate(formData, FormSchema.formCreateApp); if (validationResult.errors) { throw new Meteor.Error(400, 'Validation Failed.', validationResult.errors); } else { var cleaned = validationResult.cleaned; - var appObj = { - name: cleaned.name, - imageId: cleaned.imageId, - status: 'STARTING', - config: {} - }; - var appId = Apps.insert(appObj); - var appPath = path.join(KITE_PATH, appObj.name); + var appName = cleaned.name; + var appPath = path.join(KITE_PATH, appName); if (!fs.existsSync(appPath)) { - console.log('Created Kite ' + appObj.name + ' directory.'); + console.log('Created Kite ' + appName + ' directory.'); fs.mkdirSync(appPath, function (err) { if (err) { throw err; } }); } - Apps.update(appId, { - $set: { - 'config.APP_ID': appId, - path: appPath - } - }); - var image = Images.findOne(appObj.imageId); - loadKiteVolumes(image.path, appObj.name); - var app = Apps.findOne(appId); - removeBindFolder(app.name, function (err) { - if (err) { - console.error(err); - } - Fiber(function () { - Meteor.call('runApp', app, function (err) { - if (err) { throw err; } - }); - }).run(); - }); + var appObj = { + name: appName, + imageId: cleaned.imageId, + status: 'STARTING', + config: {}, + path: appPath + }; + Apps.insert(appObj); } }, getAppLogs: function (appId) { this.unblock(); var app = Apps.findOne(appId); if (app) { - getAppLogs(app, function (err) { + Apps.logs(app, function (err) { if (err) { throw err; } }); } @@ -117,12 +137,13 @@ Meteor.methods({ Apps.update(app._id, {$set: { status: 'STARTING' }}); - restartApp(app, function (err) { + Apps.restart(app, function (err) { if (err) { console.error(err); } }); } }, resolveWatchers: function () { + this.unblock(); return Meteor._wrapAsync(resolveWatchers)(); } }); diff --git a/meteor/server/constants.js b/meteor/server/constants.js new file mode 100644 index 0000000000..cb35d90451 --- /dev/null +++ b/meteor/server/constants.js @@ -0,0 +1,24 @@ +KITE_PATH = path.join(Util.getHomePath(), 'Kitematic'); +KITE_TAR_PATH = path.join(KITE_PATH, '.tar'); +KITE_IMAGES_PATH = path.join(KITE_PATH, '.images'); + +if (!fs.existsSync(KITE_PATH)) { + console.log('Created Kitematic directory.'); + fs.mkdirSync(KITE_PATH, function (err) { + if (err) { throw err; } + }); +} + +if (!fs.existsSync(KITE_TAR_PATH)) { + console.log('Created Kitematic .tar directory.'); + fs.mkdirSync(KITE_TAR_PATH, function (err) { + if (err) { throw err; } + }); +} + +if (!fs.existsSync(KITE_IMAGES_PATH)) { + console.log('Created Kitematic .images directory.'); + fs.mkdirSync(KITE_IMAGES_PATH, function (err) { + if (err) { throw err; } + }); +} diff --git a/meteor/server/docker.js b/meteor/server/docker.js index d71b12688b..37a27d6d88 100755 --- a/meteor/server/docker.js +++ b/meteor/server/docker.js @@ -1,16 +1,11 @@ -Docker = Meteor.require('dockerode'); +Dockerode = Meteor.require('dockerode'); -var Convert = Meteor.require('ansi-to-html'); -var convert = new Convert(); +var DOCKER_HOST='192.168.60.103'; +docker = new Dockerode({host: DOCKER_HOST, port: '2375'}); -var DOCKER_HOST='192.168.59.103'; -docker = new Docker({host: '192.168.59.103', port: '2375'}); +Docker = {}; -hasDockerfile = function (directory) { - return fs.existsSync(path.join(directory, 'Dockerfile')); -}; - -removeContainer = function (containerId, callback) { +Docker.removeContainer = function (containerId, callback) { var container = docker.getContainer(containerId); container.kill(function (err) { if (err) { callback(err); return; } @@ -22,28 +17,11 @@ removeContainer = function (containerId, callback) { }); }; -removeContainerSync = function (containerId) { - return Meteor._wrapAsync(removeContainer)(containerId); +Docker.removeContainerSync = function (containerId) { + return Meteor._wrapAsync(Docker.removeContainer)(containerId); }; -deleteApp = function (app, callback) { - if (!app.docker) { - callback(null); - return; - } - try { - removeContainerSync(app.docker.Id); - } catch (e) { - console.error(e); - } - callback(null); -}; - -deleteAppSync = function (app) { - return Meteor._wrapAsync(deleteApp)(app); -}; - -getContainerData = function (containerId, callback) { +Docker.getContainerData = function (containerId, callback) { var container = docker.getContainer(containerId); container.inspect(function (err, data) { if (err) { @@ -59,11 +37,11 @@ getContainerData = function (containerId, callback) { }); }; -getContainerDataSync = function (containerId) { - return Meteor._wrapAsync(getContainerData)(containerId); +Docker.getContainerDataSync = function (containerId) { + return Meteor._wrapAsync(Docker.getContainerData)(containerId); }; -runContainer = function (app, image, callback) { +Docker.runContainer = function (app, image, callback) { var envParam = []; _.each(_.keys(app.config), function (key) { var builtStr = key + '=' + app.config[key]; @@ -100,11 +78,11 @@ runContainer = function (app, image, callback) { }); }; -runContainerSync = function (app, image) { - return Meteor._wrapAsync(runContainer)(app, image); +Docker.runContainerSync = function (app, image) { + return Meteor._wrapAsync(Docker.runContainer)(app, image); }; -restartContainer = function (containerId, callback) { +Docker.restartContainer = function (containerId, callback) { var container = docker.getContainer(containerId); container.restart(function (err) { if (err) { @@ -117,83 +95,8 @@ restartContainer = function (containerId, callback) { }); }; -restartContainerSync = function (containerId) { - return Meteor._wrapAsync(restartContainer)(containerId); -}; - -var getFromImage = function (dockerfile) { - var patternString = "(FROM)(.*)"; - var regex = new RegExp(patternString, "g"); - var fromInstruction = dockerfile.match(regex); - if (fromInstruction && fromInstruction.length > 0) { - return fromInstruction[0].replace('FROM', '').trim(); - } else { - return null; - } -}; - -restartApp = function (app, callback) { - if (app.docker && app.docker.Id) { - try { - restartContainerSync(app.docker.Id); - } catch (e) { - console.error(e); - } - var containerData = getContainerDataSync(app.docker.Id); - Fiber(function () { - Apps.update(app._id, {$set: { - status: 'READY', - docker: containerData - }}); - }).run(); - callback(null); - // Use dig to refresh the DNS - exec('/usr/bin/dig dig ' + app.name + '.dev @172.17.42.1 ', function() {}); - } else { - callback(null); - } -}; - -getAppLogs = function (app) { - if (app.docker && app.docker.Id) { - var container = docker.getContainer(app.docker.Id); - container.logs({follow: false, stdout: true, stderr: true, timestamps: true, tail: 300}, function (err, response) { - if (err) { throw err; } - Fiber(function () { - Apps.update(app._id, { - $set: { - logs: [] - } - }); - }).run(); - var logs = []; - response.setEncoding('utf8'); - response.on('data', function (line) { - logs.push(convert.toHtml(line.slice(8))); - Fiber(function () { - Apps.update(app._id, { - $set: { - logs: logs - } - }); - }).run(); - }); - response.on('end', function () {}); - }); - } -}; - -createTarFile = function (image, callback) { - var TAR_PATH = path.join(KITE_TAR_PATH, image._id + '.tar'); - exec('tar czf ' + TAR_PATH + ' -C ' + image.path + ' .', function (err) { - if (err) { callback(err, null); return; } - console.log('Created tar file: ' + TAR_PATH); - callback(null, TAR_PATH); - }); -}; - -createTarFileSync = function (image) { - return Meteor._wrapAsync(createTarFile)(image); +Docker.restartContainerSync = function (containerId) { + return Meteor._wrapAsync(Docker.restartContainer)(containerId); }; var convertVolumeObjToArray = function (obj) { @@ -209,7 +112,7 @@ var convertVolumeObjToArray = function (obj) { return result; }; -getImageData = function (imageId, callback) { +Docker.getImageData = function (imageId, callback) { var image = docker.getImage(imageId.toLowerCase()); image.inspect(function (err, data) { if (err) { @@ -224,11 +127,11 @@ getImageData = function (imageId, callback) { }); }; -getImageDataSync = function (imageId) { - return Meteor._wrapAsync(getImageData)(imageId); +Docker.getImageDataSync = function (imageId) { + return Meteor._wrapAsync(Docker.getImageData)(imageId); }; -removeImage = function (imageId, callback) { +Docker.removeImage = function (imageId, callback) { var image = docker.getImage(imageId.toLowerCase()); image.remove({force: true}, function (err) { if (err) { callback(err); return; } @@ -237,25 +140,14 @@ removeImage = function (imageId, callback) { }); }; -removeImageSync = function (imageId) { - return Meteor._wrapAsync(removeImage)(imageId); +Docker.removeImageSync = function (imageId) { + return Meteor._wrapAsync(Docker.removeImage)(imageId); }; -deleteImage = function (image, callback) { - if (!image.docker) { - callback(null, {}); - return; - } - try { - removeImageSync(image.docker.Id); - } catch (e) { - console.error(e); - } - callback(null); -}; - -deleteImageSync = function (image) { - return Meteor._wrapAsync(deleteImage)(image); +Docker.removeBindFolder = function (name, callback) { + exec(path.join(Util.getBinDir(), 'boot2docker') + ' ssh "sudo rm -rf /var/lib/docker/binds/' + name + '"', function (err, stdout) { + callback(err, stdout); + }); }; var defaultContainerOptions = function () { @@ -304,7 +196,7 @@ resolveDefaultImages = function () { image.inspect(function (err) { if (err) { if (err.reason === 'no such image') { - docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) { + docker.loadImage(path.join(Util.getBinDir(), 'base-images.tar.gz'), {}, function (err) { if (err) { innerCallback(err); return; @@ -374,7 +266,7 @@ reloadDefaultContainers = function (callback) { async.until(function () { return ready; }, function (callback) { - docker.listContainers(function (err, containers) { + docker.listContainers(function (err) { if (!err) { ready = true; } @@ -391,7 +283,7 @@ reloadDefaultContainers = function (callback) { return; } console.log('Loading new Kitematic default images.'); - docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) { + docker.loadImage(path.join(Util.getBinDir(), 'base-images.tar.gz'), {}, function (err) { if (err) { callback(err); return; @@ -500,142 +392,18 @@ killAndRemoveContainers = function (names, callback) { }); }; -pullImageFromDockerfile = function (dockerfile, imageId, callback) { - var fromImage = getFromImage(dockerfile); - console.log('From image: ' + fromImage); - var installedImage = null; - try { - installedImage = getImageDataSync(fromImage); - } catch (e) { - console.error(e); - } - if (fromImage && !installedImage) { - Fiber(function () { - Images.update(imageId, { - $set: { - buildLogs: [] - } - }); - }).run(); - var logs = []; - docker.pull(fromImage, function (err, response) { - if (err) { callback(err); return; } - response.setEncoding('utf8'); - response.on('data', function (data) { - try { - var logData = JSON.parse(data); - var logDisplay = ''; - if (logData.id) { - logDisplay += logData.id + ' | '; - } - logDisplay += logData.status; - if (logData.progressDetail && logData.progressDetail.current && logData.progressDetail.total) { - logDisplay += ' - ' + Math.round(logData.progressDetail.current / logData.progressDetail.total * 100) + '%'; - } - logs.push(logDisplay); - Fiber(function () { - Images.update(imageId, { - $set: { - buildLogs: logs - } - }); - }).run(); - } catch (e) { - console.error(e); - } - }); - response.on('end', function () { - console.log('Finished pulling image: ' + fromImage); - callback(null); - }); - }); - } else { - callback(null); - } -}; - -buildImage = function (image, callback) { - Fiber(function () { - var tarFilePath = createTarFileSync(image); - Images.update(image._id, { - $set: { - buildLogs: [] - } - }); - docker.buildImage(tarFilePath, {t: image._id.toLowerCase()}, function (err, response) { - if (err) { callback(err); } - console.log('Building Docker image...'); - var logs = []; - response.setEncoding('utf8'); - response.on('data', function (data) { - try { - var line = JSON.parse(data).stream; - logs.push(convert.toHtml(line)); - Fiber(function () { - Images.update(image._id, { - $set: { - buildLogs: logs - } - }); - }).run(); - } catch (e) { - console.error(e); - } - }); - response.on('end', function () { - console.log('Finished building Docker image.'); - try { - fs.unlinkSync(tarFilePath); - console.log('Cleaned up tar file.'); - } catch (e) { - console.error(e); - } - Fiber(function () { - var imageData = null; - try { - imageData = getImageDataSync(image._id); - Images.update(image._id, { - $set: { - docker: imageData, - status: 'READY' - } - }); - } catch (e) { - console.log(e); - Images.update(image._id, { - $set: { - status: 'ERROR' - } - }); - } - var oldImageId = null; - if (image.docker && image.docker.Id) { - oldImageId = image.docker.Id; - } - if (oldImageId && imageData && oldImageId !== imageData.Id) { - try { - removeImageSync(oldImageId); - } catch (e) { - console.error(e); - } - } - }).run(); - callback(null); - }); - }); - }).run(); -}; - Meteor.methods({ runApp: function (app) { this.unblock(); var image = Images.findOne({_id: app.imageId}); + // Delete old container if one already exists try { - removeContainerSync(app.name); + Docker.removeContainerSync(app.name); } catch (e) {} try { - var container = runContainerSync(app, image); - var containerData = getContainerDataSync(container.id); + var container = Docker.runContainerSync(app, image); + var containerData = Docker.getContainerDataSync(container.id); + // Set a delay for app to spin up Meteor.setTimeout(function () { Apps.update(app._id, {$set: { docker: containerData, @@ -650,18 +418,23 @@ Meteor.methods({ return DOCKER_HOST; }, reloadDefaultContainers: function () { + this.unblock(); return Meteor._wrapAsync(reloadDefaultContainers)(); }, checkDefaultImages: function () { + this.unblock(); return Meteor._wrapAsync(checkDefaultImages)(); }, resolveDefaultImages: function () { + this.unblock(); return Meteor._wrapAsync(resolveDefaultImages)(); }, checkDefaultContainers: function () { + this.unblock(); return Meteor._wrapAsync(checkDefaultContainers)(); }, resolveDefaultContainers: function () { + this.unblock(); return Meteor._wrapAsync(resolveDefaultContainers)(); } }); diff --git a/meteor/server/images.js b/meteor/server/images.js index 5429dd53b1..6ad43fe7d0 100755 --- a/meteor/server/images.js +++ b/meteor/server/images.js @@ -1,4 +1,38 @@ -getImageMetaData = function (directory) { +var createTarFile = function (image, callback) { + var TAR_PATH = path.join(KITE_TAR_PATH, image._id + '.tar'); + exec('tar czf ' + TAR_PATH + ' -C ' + image.path + ' .', function (err) { + if (err) { callback(err, null); return; } + console.log('Created tar file: ' + TAR_PATH); + callback(null, TAR_PATH); + }); +}; + +var createTarFileSync = function (image) { + return Meteor._wrapAsync(createTarFile)(image); +}; + +var getFromImage = function (dockerfile) { + var patternString = "(FROM)(.*)"; + var regex = new RegExp(patternString, "g"); + var fromInstruction = dockerfile.match(regex); + if (fromInstruction && fromInstruction.length > 0) { + return fromInstruction[0].replace('FROM', '').trim(); + } else { + return null; + } +}; + +var getImageJSON = function (directory) { + var KITE_JSON_PATH = path.join(directory, 'image.json'); + if (fs.existsSync(KITE_JSON_PATH)) { + var data = fs.readFileSync(KITE_JSON_PATH, 'utf8'); + return JSON.parse(data); + } else { + return null; + } +}; + +var getImageMetaData = function (directory) { var kiteJSON = getImageJSON(directory); if (kiteJSON) { if (!kiteJSON.name) { @@ -12,8 +46,24 @@ getImageMetaData = function (directory) { return kiteJSON; }; -rebuildImage = function (image, callback) { - deleteFolder(image.path); +Images.saveFolder = function (directory, imageId, callback) { + var destinationPath = path.join(KITE_IMAGES_PATH, imageId); + if (!fs.existsSync(destinationPath)) { + fs.mkdirSync(destinationPath, function (err) { + if (err) { callback(err); return; } + }); + Util.copyFolder(directory, destinationPath); + console.log('Copied image folder for: ' + imageId); + callback(null); + } +}; + +Images.saveFolderSync = function (directory, imageId) { + return Meteor._wrapAsync(Images.saveFolder)(directory, imageId); +}; + +Images.rebuild = function (image, callback) { + Util.deleteFolder(image.path); var imageMetaData = getImageMetaData(image.originPath); if (imageMetaData.logo) { Images.update(image._id, { @@ -35,18 +85,144 @@ rebuildImage = function (image, callback) { } }); image = Images.findOne(image._id); - saveImageFolderSync(image.originPath, image._id); - pullImageFromDockerfile(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) { + Images.saveFolderSync(image.originPath, image._id); + Images.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) { if (err) { callback(err, null); return; } - buildImage(image, function (err) { + Images.build(image, function (err) { if (err) { console.error(err); } callback(null, null); }); }); }; -rebuildImageSync = function (image) { - return Meteor._wrapAsync(rebuildImage)(image); +Images.rebuildSync = function (image) { + return Meteor._wrapAsync(Images.rebuild)(image); +}; + +Images.pull = function (dockerfile, imageId, callback) { + var fromImage = getFromImage(dockerfile); + console.log('From image: ' + fromImage); + var installedImage = null; + try { + installedImage = Docker.getImageDataSync(fromImage); + } catch (e) { + console.error(e); + } + if (fromImage && !installedImage) { + Fiber(function () { + Images.update(imageId, { + $set: { + buildLogs: [] + } + }); + }).run(); + var logs = []; + docker.pull(fromImage, function (err, response) { + if (err) { callback(err); return; } + response.setEncoding('utf8'); + response.on('data', function (data) { + try { + var logData = JSON.parse(data); + var logDisplay = ''; + if (logData.id) { + logDisplay += logData.id + ' | '; + } + logDisplay += logData.status; + if (logData.progressDetail && logData.progressDetail.current && logData.progressDetail.total) { + logDisplay += ' - ' + Math.round(logData.progressDetail.current / logData.progressDetail.total * 100) + '%'; + } + logs.push(logDisplay); + Fiber(function () { + Images.update(imageId, { + $set: { + buildLogs: logs + } + }); + }).run(); + } catch (e) { + console.error(e); + } + }); + response.on('end', function () { + console.log('Finished pulling image: ' + fromImage); + callback(null); + }); + }); + } else { + callback(null); + } +}; + +Images.build = function (image, callback) { + Fiber(function () { + var tarFilePath = createTarFileSync(image); + Images.update(image._id, { + $set: { + buildLogs: [] + } + }); + docker.buildImage(tarFilePath, {t: image._id.toLowerCase()}, function (err, response) { + if (err) { callback(err); } + console.log('Building Docker image...'); + var logs = []; + response.setEncoding('utf8'); + response.on('data', function (data) { + try { + var line = JSON.parse(data).stream; + logs.push(convert.toHtml(line)); + Fiber(function () { + Images.update(image._id, { + $set: { + buildLogs: logs + } + }); + }).run(); + } catch (e) { + console.error(e); + } + }); + response.on('end', function () { + console.log('Finished building Docker image.'); + try { + fs.unlinkSync(tarFilePath); + console.log('Cleaned up tar file.'); + } catch (e) { + console.error(e); + } + Fiber(function () { + var imageData = null; + try { + imageData = Docker.getImageDataSync(image._id); + Images.update(image._id, { + $set: { + docker: imageData, + status: 'READY' + } + }); + } catch (e) { + console.log(e); + Images.update(image._id, { + $set: { + status: 'ERROR' + } + }); + } + var oldImageId = null; + if (image.docker && image.docker.Id) { + oldImageId = image.docker.Id; + } + if (oldImageId && imageData && oldImageId !== imageData.Id) { + try { + Docker.removeImageSync(oldImageId); + } catch (e) { + console.error(e); + } + } + }).run(); + callback(null); + }); + }); + }).run(); }; Meteor.methods({ @@ -58,29 +234,7 @@ Meteor.methods({ }; var imageMetaData = getImageMetaData(directory); imageObj.meta = imageMetaData; - var imageId = Images.insert(imageObj); - var imagePath = path.join(KITE_IMAGES_PATH, imageId); - Images.update(imageId, { - $set: { - path: imagePath - } - }); - if (imageObj.meta.logo) { - Images.update(imageId, { - $set: { - logoPath: path.join(imagePath, imageObj.meta.logo) - } - }); - } - var image = Images.findOne(imageId); - saveImageFolderSync(directory, imageId); - console.log('Saved folder sync'); - pullImageFromDockerfile(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), imageId, function (err) { - if (err) { throw err; } - buildImage(image, function (err) { - if (err) { console.error(err); } - }); - }); + Images.insert(imageObj); }, rebuildImage: function (imageId) { this.unblock(); @@ -92,7 +246,13 @@ Meteor.methods({ if (apps.length > 0) { _.each(apps, function (app) { console.log('Updating app: ' + app.name); - deleteAppSync(app); + if (app.docker) { + try { + Docker.removeContainerSync(app.docker.Id); + } catch (e) { + console.error(e); + } + } Apps.update(app._id, { $set: { 'docker.Id': null, @@ -101,7 +261,7 @@ Meteor.methods({ } }); }); - rebuildImageSync(image); + Images.rebuildSync(image); _.each(apps, function (app) { app = Apps.findOne(app._id); Meteor.call('runApp', app, function (err) { @@ -109,7 +269,7 @@ Meteor.methods({ }); }); } else { - rebuildImageSync(image); + Images.rebuildSync(image); } }, changeDirectory: function (imageId, directory) { @@ -125,7 +285,8 @@ Meteor.methods({ }); }, validateDirectory: function (directory) { - if (!hasDockerfile(directory)) { + this.unblock(); + if (!Util.hasDockerfile(directory)) { throw new Meteor.Error(400, "Only directories with Dockerfiles are supported now."); } }, @@ -137,15 +298,7 @@ Meteor.methods({ } var app = Apps.findOne({imageId: imageId}); if (!app) { - console.log('here'); - try { - deleteImageSync(image); - deleteFolder(image.path); - } catch (e) { - console.log(e); - } finally { - Images.remove({_id: image._id}); - } + Images.remove({_id: image._id}); } else { throw new Meteor.Error(400, 'This image is currently being used by ' + app.name + "."); } diff --git a/meteor/server/kite.js b/meteor/server/kite.js deleted file mode 100755 index 712635616a..0000000000 --- a/meteor/server/kite.js +++ /dev/null @@ -1,59 +0,0 @@ -KITE_PATH = path.join(getHomePath(), 'Kitematic'); -KITE_TAR_PATH = path.join(KITE_PATH, '.tar'); -KITE_IMAGES_PATH = path.join(KITE_PATH, '.images'); - -if (!fs.existsSync(KITE_PATH)) { - console.log('Created Kitematic directory.'); - fs.mkdirSync(KITE_PATH, function (err) { - if (err) { throw err; } - }); -} - -if (!fs.existsSync(KITE_TAR_PATH)) { - console.log('Created Kitematic .tar directory.'); - fs.mkdirSync(KITE_TAR_PATH, function (err) { - if (err) { throw err; } - }); -} - -if (!fs.existsSync(KITE_IMAGES_PATH)) { - console.log('Created Kitematic .images directory.'); - fs.mkdirSync(KITE_IMAGES_PATH, function (err) { - if (err) { throw err; } - }); -} - -getImageJSON = function (directory) { - var KITE_JSON_PATH = path.join(directory, 'image.json'); - if (fs.existsSync(KITE_JSON_PATH)) { - var data = fs.readFileSync(KITE_JSON_PATH, 'utf8'); - return JSON.parse(data); - } else { - return null; - } -}; - -loadKiteVolumes = function (directory, appName) { - var KITE_VOLUMES_PATH = path.join(directory, 'volumes'); - if (fs.existsSync(KITE_VOLUMES_PATH)) { - var destinationPath = path.join(KITE_PATH, appName); - copyFolder(KITE_VOLUMES_PATH, destinationPath); - console.log('Copied volumes for: ' + appName); - } -}; - -saveImageFolder = function (directory, imageId, callback) { - var destinationPath = path.join(KITE_IMAGES_PATH, imageId); - if (!fs.existsSync(destinationPath)) { - fs.mkdirSync(destinationPath, function (err) { - if (err) { callback(err); return; } - }); - copyFolder(directory, destinationPath); - console.log('Copied image folder for: ' + imageId); - callback(null); - } -}; - -saveImageFolderSync = function (directory, imageId) { - return Meteor._wrapAsync(saveImageFolder)(directory, imageId); -}; diff --git a/meteor/server/lib/requires.js b/meteor/server/lib/requires.js index cac98d255a..6174cd0202 100755 --- a/meteor/server/lib/requires.js +++ b/meteor/server/lib/requires.js @@ -6,4 +6,6 @@ path = Meteor.require('path'); exec = Meteor.require('child_process').exec; async = Meteor.require('async'); Fiber = Meteor.require('fibers'); -child_process = Meteor.require('child_process'); \ No newline at end of file +child_process = Meteor.require('child_process'); +Convert = Meteor.require('ansi-to-html'); +convert = new Convert(); diff --git a/meteor/server/lib/utilities.js b/meteor/server/lib/utilities.js deleted file mode 100755 index 6b4d68df1a..0000000000 --- a/meteor/server/lib/utilities.js +++ /dev/null @@ -1,53 +0,0 @@ -getHomePath = function () { - return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; -}; - -getBinDir = function () { - if (process.env.NODE_ENV === 'development') { - return path.join(path.join(process.env.PWD, '..'), 'resources'); - } else { - return path.join(process.cwd(), '../../../resources'); - } -}; - -deleteFolder = function (directory) { - if (fs.existsSync(directory)) { - fs.readdirSync(directory).forEach(function (file) { - var curDirectory = directory + '/' + file; - if (fs.lstatSync(curDirectory).isDirectory()) { - // Recurse - deleteFolder(curDirectory); - } else { - // Delete File - try { - fs.unlinkSync(curDirectory); - } catch (e) { - console.error(e); - } - } - }); - fs.rmdirSync(directory); - } -}; - -copyFolder = function (src, dest) { - var exists = fs.existsSync(src); - var stats = exists && fs.statSync(src); - var isDirectory = exists && stats.isDirectory(); - if (exists && isDirectory) { - try { - fs.mkdirSync(dest); - } catch (e) { - console.error(e); - } - fs.readdirSync(src).forEach(function (childItemName) { - copyFolder(path.join(src, childItemName), path.join(dest, childItemName)); - }); - } else { - try { - fs.linkSync(src, dest); - } catch (e) { - console.error(e); - } - } -}; diff --git a/meteor/server/publications.js b/meteor/server/publications.js index fe75a8a9ba..6e3540fc61 100755 --- a/meteor/server/publications.js +++ b/meteor/server/publications.js @@ -7,5 +7,5 @@ Meteor.publish('images', function () { }); Meteor.publish('installs', function () { - return Installs.find({}, {sort: {createdAt: -1}}); + return Installs.find({}); }); diff --git a/resources/install b/resources/install index 723ff280d1..6f8f7c122f 100755 --- a/resources/install +++ b/resources/install @@ -1,45 +1,39 @@ #!/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 -echo "nameserver 172.17.42.1" > /etc/resolver/dev +echo "nameserver 172.17.42.1" > /etc/resolver/kite DIR=$(dirname "$0") USER=`w -h | sort -u -t' ' -k1,1 | awk '{print $1}'` /bin/rm -rf /Library/LaunchAgents/com.kitematic.route.plist - -echo ' - - +echo " + + Label com.kitematic.route ProgramArguments - /sbin/route - -n - add - 172.17.0.0/16 - 192.168.59.103 + bash + -c + /usr/sbin/scutil -w State:/Network/Interface/$IFACE/IPv4;/sbin/route -n add -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY KeepAlive RunAtLoad - ServiceIPC - - UserName - root LaunchOnlyOnce -' > /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 +" > /Library/LaunchAgents/com.kitematic.route.plist # Add entries to routing table for Kitematic VM -/sbin/route delete 172.17.0.0/16 192.168.59.103 -/sbin/route -n add 172.17.0.0/16 192.168.59.103 \ No newline at end of file +/sbin/route delete -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY +/sbin/route -n add -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY \ No newline at end of file diff --git a/resources/kite-dns/dnsmasq.tmpl b/resources/kite-dns/dnsmasq.tmpl index 546fda7910..0355b257cc 100644 --- a/resources/kite-dns/dnsmasq.tmpl +++ b/resources/kite-dns/dnsmasq.tmpl @@ -1,6 +1,6 @@ user=root {{ range $index, $value := $ }} {{ with $address := index $value.Addresses 0 }} -address=/{{$value.Name}}.dev/{{$address.IP}} +address=/{{$value.Name}}.kite/{{$address.IP}} {{ end }} {{ end }} diff --git a/script/setup.sh b/script/setup.sh index c40c686e78..217e096da2 100755 --- a/script/setup.sh +++ b/script/setup.sh @@ -15,6 +15,7 @@ pushd cache if [ ! -f $BASE_IMAGE_VERSION_FILE ]; then 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 + cp $BASE_IMAGE_VERSION_FILE ../resources/$BASE_IMAGE_FILE fi if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then @@ -60,11 +61,6 @@ if [ ! -f $COCOASUDO_FILE ]; then chmod +x $COCOASUDO_FILE 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 chmod +x $BOOT2DOCKER_CLI_FILE diff --git a/script/versions.sh b/script/versions.sh index f1f7853a2f..e46149f22c 100644 --- a/script/versions.sh +++ b/script/versions.sh @@ -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_FILE=base-images.tar.gz