diff --git a/index.js b/index.js index 5b692ca4c9..c5d7ee2547 100644 --- a/index.js +++ b/index.js @@ -3,12 +3,14 @@ var net = require('net'); var os = require('os'); var fs = require('fs'); var path = require('path'); +var exec = require('exec'); +var autoUpdater = require('auto-updater'); var app = require('app'); var BrowserWindow = require('browser-window'); +var ipc = require('ipc'); var dirname = __dirname; -console.log(dirname); var freeport = function (callback) { var server = net.createServer(); @@ -116,33 +118,75 @@ app.on('activate-with-no-open-windows', function () { app.on('ready', function() { start(function (url, nodeChild, mongoChild) { - var cleanUpChildren = function () { - console.log('Cleaning up children.'); - mongoChild.kill(); - nodeChild.kill(); - app.quit(); - process.exit(); - }; - - if (nodeChild && mongoChild) { - process.on('exit', cleanUpChildren); - process.on('uncaughtException', cleanUpChildren); - process.on('SIGINT', cleanUpChildren); - process.on('SIGTERM', cleanUpChildren); - } // Create the browser window. var windowOptions = { width: 800, height: 578, - frame: false, resizable: false, + frame: false, 'web-preferences': { 'web-security': false } }; mainWindow = new BrowserWindow(windowOptions); - mainWindow.focus(); + mainWindow.hide(); mainWindow.loadUrl(url); + + process.on('uncaughtException', app.quit); + + var saveVMOnQuit = true; + app.on('will-quit', function (e) { + console.log('Cleaning up children.'); + if (nodeChild) { + nodeChild.kill(); + } + if (mongoChild) { + mongoChild.kill(); + } + if (saveVMOnQuit) { + exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {}); + } + }); + + mainWindow.webContents.on('new-window', function (e) { + e.preventDefault(); + }); + + mainWindow.webContents.on('did-finish-load', function() { + mainWindow.show(); + mainWindow.focus(); + + // Auto Updates + autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion()); + + autoUpdater.on('checking-for-update', function (e) { + console.log('Checking for update...'); + }); + + autoUpdater.on('update-available', function (e) { + console.log('Update available.'); + console.log(e); + }); + + autoUpdater.on('update-not-available', function (e) { + console.log('Update not available.'); + }); + + autoUpdater.on('update-downloaded', function (e, releaseNotes, releaseName, releaseDate, updateURL) { + console.log('Update downloaded.'); + mainWindow.webContents.send('notify', 'window:update-available'); + }); + + ipc.on('command', function (event, arg) { + console.log('Command: ' + arg); + if (arg === 'application:quit-install') { + saveVMOnQuit = false; + autoUpdater.quitAndInstall(); + } + }); + + autoUpdater.checkForUpdates(); + }); }); }); diff --git a/meteor/.meteor/release b/meteor/.meteor/release index 7057f80807..87bf495278 100755 --- a/meteor/.meteor/release +++ b/meteor/.meteor/release @@ -1 +1 @@ -METEOR@0.9.4 +METEOR@1.0 diff --git a/meteor/.meteor/versions b/meteor/.meteor/versions index 7664d0329a..c821689bcb 100644 --- a/meteor/.meteor/versions +++ b/meteor/.meteor/versions @@ -1,16 +1,16 @@ application-configuration@1.0.3 -autoupdate@1.1.2 +autoupdate@1.1.3 base64@1.0.1 binary-heap@1.0.1 blaze-tools@1.0.1 -blaze@2.0.2 +blaze@2.0.3 boilerplate-generator@1.0.1 callback-hook@1.0.1 check@1.0.2 ctl-helper@1.0.4 ctl@1.0.2 dburles:collection-helpers@1.0.1 -ddp@1.0.10 +ddp@1.0.11 deps@1.0.5 ejson@1.0.4 fastclick@1.0.1 @@ -18,23 +18,28 @@ follower-livedata@1.0.2 geojson-utils@1.0.1 html-tools@1.0.2 htmljs@1.0.2 -http@1.0.7 +http@1.0.8 id-map@1.0.1 -iron:core@0.3.4 -iron:dynamic-template@0.4.1 -iron:layout@0.4.1 -iron:router@0.9.4 +iron:controller@1.0.2 +iron:core@1.0.2 +iron:dynamic-template@1.0.2 +iron:layout@1.0.2 +iron:location@1.0.2 +iron:middleware-stack@1.0.2 +iron:router@1.0.2 +iron:url@1.0.2 jquery@1.0.1 json@1.0.1 -less@1.0.10 +launch-screen@1.0.0 +less@1.0.11 livedata@1.0.11 -logging@1.0.4 -meteor-platform@1.1.2 -meteor@1.1.2 -minifiers@1.1.1 -minimongo@1.0.4 +logging@1.0.5 +meteor-platform@1.2.0 +meteor@1.1.3 +minifiers@1.1.2 +minimongo@1.0.5 mobile-status-bar@1.0.1 -mongo@1.0.7 +mongo@1.0.8 mrt:underscore-string-latest@2.3.3 observe-sequence@1.0.3 ordered-dict@1.0.1 @@ -44,17 +49,17 @@ reactive-dict@1.0.4 reactive-var@1.0.3 reload@1.1.1 retry@1.0.1 -reywood:iron-router-ga@0.3.2 +reywood:iron-router-ga@0.4.1 routepolicy@1.0.2 -session@1.0.3 +session@1.0.4 simison:bootstrap3-less@0.3.0 spacebars-compiler@1.0.3 spacebars@1.0.3 standard-app-packages@1.0.3 -templating@1.0.8 +templating@1.0.9 tracker@1.0.3 ui@1.0.4 underscore@1.0.1 -url@1.0.1 +url@1.0.2 webapp-hashing@1.0.1 -webapp@1.1.3 +webapp@1.1.4 diff --git a/meteor/client/lib/apputil.js b/meteor/client/lib/apputil.js index 5ae1610f14..8ceb1c8133 100644 --- a/meteor/client/lib/apputil.js +++ b/meteor/client/lib/apputil.js @@ -1,52 +1,33 @@ var exec = require('exec'); var path = require('path'); var fs = require('fs'); +var async = require('async'); var Convert = require('ansi-to-html'); var convert = new Convert(); AppUtil = {}; -AppUtil.run = function (app) { +AppUtil.run = function (app, callback) { var image = Images.findOne({_id: app.imageId}); - // Delete old container if one already exists + Apps.update(app._id, {$set: { + status: 'STARTING' + }}); Docker.removeContainer(app.name, function (err) { - if (err) { console.error(err); } Docker.runContainer(app, image, function (err, container) { - if (err) { throw err; } + if (err) { callback(err); } Docker.getContainerData(container.id, function (err, data) { - if (err) { console.error(err); } + if (err) { callback(err); } // Set a delay for app to spin up - Meteor.setTimeout(function () { - Apps.update(app._id, {$set: { - docker: data, - status: 'READY' - }}); - }, 2500); + Apps.update(app._id, {$set: { + docker: data, + status: 'READY' + }}); + callback(); }); }); }); }; -AppUtil.restartHelper = function (app) { - if (app.docker && app.docker.Id) { - Docker.restartContainer(app.docker.Id, function (err) { - if (err) { console.error(err); } - Docker.getContainerData(app.docker.Id, function (err, data) { - if (err) { console.error(err); } - Util.refreshDNS(app, function (err) { - if (err) { - console.error(err); - } - Apps.update(app._id, {$set: { - status: 'READY', - docker: data - }}); - }); - }); - }); - } -}; - AppUtil.start = function (appId) { var app = Apps.findOne(appId); if (app && app.docker) { @@ -57,15 +38,10 @@ AppUtil.start = function (appId) { if (err) { console.error(err); } Docker.getContainerData(app.docker.Id, function (err, data) { if (err) { console.error(err); } - Util.refreshDNS(app, function (err) { - if (err) { - console.error(err); - } - Apps.update(app._id, {$set: { - status: 'READY', - docker: data - }}); - }); + Apps.update(app._id, {$set: { + status: 'READY', + docker: data + }}); }); }); } @@ -88,27 +64,13 @@ AppUtil.stop = function (appId) { } }; -AppUtil.restart = function (appId) { - var app = Apps.findOne(appId); - if (app && app.docker) { - Apps.update(app._id, {$set: { - status: 'STARTING' - }}); - AppUtil.restartHelper(app); - } -}; - AppUtil.remove = function (appId) { var app = Apps.findOne(appId); - Apps.remove({_id: appId}); if (app.docker) { Docker.removeContainer(app.docker.Id, function (err) { - if (err) { console.error(err); } + Apps.remove({_id: appId}); var appPath = path.join(Util.KITE_PATH, app.name); Util.deleteFolder(appPath); - Docker.removeBindFolder(app.name, function () { - console.log('Deleted Kite ' + app.name + ' directory.'); - }); }); } }; @@ -119,7 +81,11 @@ AppUtil.configVar = function (appId, configVars) { status: 'STARTING' }}); var app = Apps.findOne({_id: appId}); - AppUtil.run(app); + AppUtil.run(Apps.findOne(appId), function (err) { + if (err) { + throw err; + } + }); }; AppUtil.logs = function (appId) { @@ -166,93 +132,112 @@ AppUtil.recover = function () { }); }; -AppUtil.sync = function () { +AppUtil.sync = function (callback) { Docker.listContainers(function (err, containers) { if (err) { - console.error(err); - } else { - var apps = Apps.find({}).fetch(); - _.each(apps, function (app) { - var app = Apps.findOne(app._id); - if (app && app.docker && app.docker.Id) { - var duplicateApps = Apps.find({'docker.Id': app.docker.Id, _id: {$ne: app._id}}).fetch(); - _.each(duplicateApps, function (duplicateApp) { - Apps.remove(duplicateApp._id); - }); - Docker.getContainerData(app.docker.Id, function (err, data) { - var status = 'STARTING'; - if (data && data.State && data.State.Running) { - status = 'READY'; - } else if (data && data.State && !data.State.Running) { - status = 'ERROR'; - } - Apps.update(app._id, { - $set: { - docker: data, - status: status - } - }) - }); - } + callback(err); + return; + } + + var apps = Apps.find({}).fetch(); + var guiIds = _.map(apps, function (app) { + if (app.docker && app.docker.Id) { + return app.docker.Id; + } + }); + var containerIds = _.map(containers, function (container) { + return container.Id; + }); + var diffApps = _.difference(guiIds, containerIds); + _.each(diffApps, function (appContainerId) { + var app = Apps.findOne({'docker.Id': appContainerId}); + if (app && app.status !== 'STARTING') { + AppUtil.remove(app._id); + } + }); + var diffContainers = _.reject(containers, function (container) { + return _.contains(guiIds, container.Id); + }); + _.each(diffContainers, function (container) { + var appName = container.Name.substring(1); + var startingApp = _.find(apps, function (app) { + return app.status === 'STARTING' && app.name === appName; }); - var dockerIds = _.map(apps, function (app) { - if (app.docker && app.docker.Id) { - return app.docker.Id; - } - }); - var containerIds = _.map(containers, function (container) { - return container.Id; - }); - var diffApps = _.difference(dockerIds, containerIds); - _.each(diffApps, function (appContainerId) { - var app = Apps.findOne({'docker.Id': appContainerId}); - if (app && app.status !== 'STARTING') { - AppUtil.remove(app._id); - } - }); - var diffContainers = _.reject(containers, function (container) { - return _.contains(dockerIds, container.Id); - }); - _.each(diffContainers, function (container) { - var appName = container.Name.substring(1); - var startingApp = _.find(apps, function (app) { - return app.status === 'STARTING' && app.name === appName; + + if (startingApp || _.isEmpty(container.NetworkSettings.Ports)) { + return; + } + + var appPath = path.join(Util.KITE_PATH, appName); + if (!fs.existsSync(appPath)) { + console.log('Created Kite ' + appName + ' directory.'); + fs.mkdirSync(appPath, function (err) { + if (err) { throw err; } }); - if (!startingApp && appName !== 'kite-dns') { - var appPath = path.join(Util.KITE_PATH, appName); - if (!fs.existsSync(appPath)) { - console.log('Created Kite ' + appName + ' directory.'); - fs.mkdirSync(appPath, function (err) { - if (err) { throw err; } - }); - } - var envVars = container.Config.Env; - var config = {}; - _.each(envVars, function (envVar) { - var eqPos = envVar.indexOf('='); - var envKey = envVar.substring(0, eqPos); - var envVal = envVar.substring(eqPos + 1); - config[envKey] = envVal; - }); + } + var envVars = container.Config.Env; + var config = {}; + _.each(envVars, function (envVar) { + var eqPos = envVar.indexOf('='); + var envKey = envVar.substring(0, eqPos); + var envVal = envVar.substring(eqPos + 1); + config[envKey] = envVal; + }); + var status = 'STARTING'; + if (container.State.Running) { + status = 'READY'; + } else { + status = 'ERROR'; + } + + var appObj = { + name: appName, + docker: container, + status: status, + config: config, + path: appPath, + logs: [], + createdAt: new Date() + }; + + var image = Images.findOne({'docker.Id': container.Image}); + if (image) { + appObj.imageId = image._id; + } + if (container.HostConfig.Binds && container.HostConfig.Binds.length) { + appObj.volumesEnabled = true; + } else { + appObj.volumesEnabled = false; + } + console.log(appObj); + Apps.insert(appObj); + }); + + async.each(apps, function (app, callback) { + if (app && app.docker && app.docker.Id) { + var duplicateApps = Apps.find({'docker.Id': app.docker.Id, _id: {$ne: app._id}}).fetch(); + _.each(duplicateApps, function (duplicateApp) { + Apps.remove(duplicateApp._id); + }); + Docker.getContainerData(app.docker.Id, function (err, data) { + if (err) {callback(err); return;} var status = 'STARTING'; - if (container.State.Running) { + if (data && data.State && data.State.Running) { status = 'READY'; - } else { + } else if (data && data.State && !data.State.Running) { status = 'ERROR'; } - var appObj = { - name: appName, - docker: container, - status: status, - config: config, - path: appPath, - logs: [], - createdAt: new Date() - }; - console.log(appObj); - Apps.insert(appObj); - } - }); - } + Apps.update(app._id, { + $set: { + docker: data, + status: status + } + }); + callback(); + }); + } + }, function (err) { + callback(err); + }); }); }; diff --git a/meteor/client/lib/boot2docker.js b/meteor/client/lib/boot2docker.js index 19966da9b5..36c44ad89d 100644 --- a/meteor/client/lib/boot2docker.js +++ b/meteor/client/lib/boot2docker.js @@ -1,24 +1,29 @@ var exec = require('exec'); var path = require('path'); var fs = require('fs'); +var path = require('path'); +var async = require('async'); +var packagejson = JSON.parse(fs.readFileSync(path.join(process.env.DIR, 'package.json'), 'utf8')); + Boot2Docker = {}; - -Boot2Docker.REQUIRED_IP = '192.168.60.103'; +Boot2Docker.VERSION = packagejson['boot2docker-version']; Boot2Docker.command = function () { - return path.join(Util.getBinDir(), 'boot2docker-1.3.0') + ' --vm="kitematic-vm"'; + return path.join(Util.getBinDir(), 'boot2docker-' + Boot2Docker.VERSION); }; Boot2Docker.exec = function (command, callback) { - exec(Boot2Docker.command() + ' ' + command, function(err, stdout, stderr) { - callback(err, stdout, stderr); + var cmd = [Boot2Docker.command()]; + cmd.push.apply(cmd, command); + exec(cmd, function(stderr, stdout, code) { + callback(stderr, stdout, code); }); }; Boot2Docker.exists = function (callback) { - this.exec('info', function (err) { - if (err) { + this.exec(['info'], function (stderr, stdout, code) { + if (stderr) { callback(null, false); } else { callback(null, true); @@ -27,49 +32,53 @@ Boot2Docker.exists = function (callback) { }; Boot2Docker.stop = function (callback) { - this.exec('stop', function (err, stdout) { - // Sometimes stop returns an error even though it worked - callback(null); + this.exec(['stop'], function (stderr, stdout, code) { + if (code) { + callback(stderr); + } else { + callback(); + } }); }; Boot2Docker.erase = function (callback) { - var VMFileLocation = path.join(Util.getHomePath(), 'VirtualBox\\ VMs/kitematic-vm'); - exec('rm -rf ' + VMFileLocation, function (err) { - callback(err); + var VMFileLocation = path.join(Util.getHomePath(), 'VirtualBox\\ VMs/boot2docker-vm'); + exec(['rm', '-rf', VMFileLocation], function (stderr) { + callback(stderr); }); }; Boot2Docker.upgrade = function (callback) { var self = this; - self.stop(function (err) { - self.exec('upgrade', function (err, stdout) { - callback(err); + self.stop(function (stderr, stdout, code) { + if (code) {callback(stderr); return;} + self.exec(['upgrade'], function (stderr, stdout, code) { + if (code) { + callback(stderr); + } else { + callback(); + } }); }); }; Boot2Docker.ip = function (callback) { - this.exec('ip', function (err, stdout) { - if (err) { - callback(err, null); + this.exec(['ip'], function (stderr, stdout, code) { + if (code) { + callback(stderr, null); } else { callback(null, stdout); } }); }; -Boot2Docker.setIp = function (ifname, ip, callback) { - Boot2Docker.exec('ssh "sudo ifconfig ' + ifname + ' ' + ip + ' netmask 255.255.255.0"', function (err, stdout) { - Boot2Docker.exec('ssh "sudo rm -rf /var/lib/boot2docker/tls/* && sudo /etc/init.d/docker restart"', function (err, stdout) { - callback(err); - }); - }); -}; - Boot2Docker.init = function (callback) { - this.exec('init', function (err) { - callback(err); + this.exec(['init'], function (stderr, stdout, code) { + if (code) { + callback(stderr); + } else { + callback(); + } }); }; @@ -80,33 +89,19 @@ Boot2Docker.start = function (callback) { callback('Cannot start if the boot2docker VM doesn\'t exist'); return; } - self.exec('start', 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') !== -1 || err.indexOf('Writing') !== -1 || err.indexOf('Generating a server cert') !== -1)) { - self.correct(function (err) { - self.injectUtilities(function (err) { - callback(err); - }); - }); + self.exec(['start'], function (stderr, stdout, code) { + if (code) { + callback(stderr); } else { - callback(err); + callback(); } }); }); }; -Boot2Docker.correct = function (callback) { - Boot2Docker.setIp('eth1', 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; } + this.exec(['info'], function (stderr, stdout, code) { + if (code) { callback(stderr, null); return; } try { var info = JSON.parse(stdout); callback(null, info.State); @@ -117,9 +112,9 @@ Boot2Docker.state = function (callback) { }; Boot2Docker.diskUsage = function (callback) { - this.exec('ssh "df"', function (err, stdout) { - if (err) { - callback(err, null); + this.exec(['ssh', 'df'], function (stderr, stdout, code) { + if (code) { + callback(stderr, null); return; } try { @@ -140,15 +135,15 @@ Boot2Docker.diskUsage = function (callback) { percent: percent }); } catch (error) { - callback(err, null); + callback(error, null); } }); }; Boot2Docker.memoryUsage = function (callback) { - this.exec('ssh "free -m"', function (err, stdout) { - if (err) { - callback(err, null); + this.exec(['ssh', 'free -m'], function (stderr, stdout, code) { + if (code) { + callback(stderr, null); return; } try { @@ -208,25 +203,10 @@ Boot2Docker.sshKeyExists = function () { return fs.existsSync(path.join(Util.getHomePath(), '.ssh', 'id_boot2docker')); }; -/** - * 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(null, stdout); - } - }); -}; - Boot2Docker.version = function (callback) { - this.exec('version', function (err, stdout, stderr) { - if (err) { - callback(err); + this.exec(['version'], function (stderr, stdout, code) { + if (code) { + callback(stderr); return; } var match = stdout.match(/Client version: v(\d\.\d\.\d)/); @@ -238,12 +218,6 @@ Boot2Docker.version = function (callback) { }); }; -Boot2Docker.injectUtilities = function (callback) { - exec('/bin/cat ' + path.join(Util.getBinDir(), 'kite-binaries.tar.gz') + ' | ' + Boot2Docker.command() + ' ssh "sudo tar zx -C /usr/local/bin && sudo chown -R root.root /usr/local/bin"', function (err, stdout) { - callback(err); - }); -}; - Boot2Docker.check = function (callback) { var self = this; self.exists(function (err, exists) { @@ -254,33 +228,6 @@ Boot2Docker.check = function (callback) { self.state(function (err, state) { if (state !== 'running') { callback('boot2docker not running'); - } else { - self.correct(function (err) { - callback(err); - }); - } - }); - } - }); -}; - -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) { - self.init(function () { - self.start(function (err) { - callback(err); - }); - }); - } else { - // If it exists but it's not running.. restart it. - self.state(function (err, state) { - if (state !== 'running') { - self.start(function (err) { - callback(err); - }); } else { callback(); } @@ -288,3 +235,49 @@ Boot2Docker.resolve = function (callback) { } }); }; + +Boot2Docker.vmUpToDate = function (callback) { + fs.readFile(path.join(Util.getHomePath(), '.boot2docker', 'boot2docker.iso'), 'utf8', function (err, data) { + if (err) { + callback(err); return; + } + var match = data.match(/Boot2Docker-v(\d+\.\d+\.\d+)/); + if (!match) { + callback('Could not parse boot2docker iso version'); + return; + } + callback (null, Util.compareVersions(match[1], Boot2Docker.VERSION) >= 0); + }); +}; + +Boot2Docker.status = function (callback) { + this.exec(['status'], function (stderr, stdout, code) { + if (code) {callback(stderr); return;} + callback(null, stdout.trim()); + }); +}; + +Boot2Docker.portAvailable = function (port, protocol, callback) { + this.exec(['ssh', 'netstat -lntu | grep LISTEN | grep ' + protocol + ' | grep -c ":::' + port + '\\s"'], function (stdout, stderr, code) { + if (stderr.trim() === '0') { + callback(true); + } else { + callback(false); + } + }); +}; + +Boot2Docker.waitWhileStatus = function (status, callback) { + var current = status; + async.whilst(function () { + return current === status; + }, function (innerCallback) { + Boot2Docker.status(function (err, vmStatus) { + if (err) {innerCallback(err); return;} + current = vmStatus.trim(); + innerCallback(); + }); + }, function (err) { + callback(err); + }); +}; \ No newline at end of file diff --git a/meteor/client/lib/docker.js b/meteor/client/lib/docker.js index 8d8333a3c2..55a3b078ba 100644 --- a/meteor/client/lib/docker.js +++ b/meteor/client/lib/docker.js @@ -6,20 +6,22 @@ var fs = require('fs'); Docker = {}; -Docker.DEFAULT_IMAGES_FILENAME = 'base-images-0.0.2.tar.gz'; -Docker.DEFAULT_IMAGES_CHECKSUM = 'a3517ac21034a1969d9ff15e3c41b1e2f1aa83c67b16a8bd0bc378ffefaf573b'; // Sha256 Checksum -Docker.HOST_IP = '192.168.60.103'; -Docker.HOST_PORT = '2376'; +Docker.hostIp = null; +Docker.hostPort = '2376'; + +Docker.setHost = function (host) { + Docker.hostIp = host; +}; Docker.client = function () { - var certDir = path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.boot2docker/certs/kitematic-vm'); + var certDir = path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.boot2docker/certs/boot2docker-vm'); if (!fs.existsSync(certDir)) { return null; } return new Dockerode({ protocol: 'https', - host: Docker.HOST_IP, - port: Docker.HOST_PORT, + host: Docker.hostIp, + port: Docker.hostPort, ca: fs.readFileSync(path.join(certDir, 'ca.pem')), cert: fs.readFileSync(path.join(certDir, 'cert.pem')), key: fs.readFileSync(path.join(certDir, 'key.pem')) @@ -93,36 +95,51 @@ Docker.runContainer = function (app, image, callback) { var builtStr = key + '=' + app.config[key]; envParam.push(builtStr); }); - console.log(envParam); - Docker.client().createContainer({ + + var containerOpts = { Image: image.docker.Id, Tty: false, Env: envParam, Hostname: app.name, name: app.name - }, function (err, container) { + }; + + + if (app.docker && app.docker.NetworkSettings.Ports) { + containerOpts.ExposedPorts = app.docker.NetworkSettings.Ports; + } + + console.log(containerOpts); + + Docker.client().createContainer(containerOpts, function (err, container) { if (err) { callback(err, null); return; } console.log('Created container: ' + container.id); // Bind volumes var binds = []; - if (image.docker.Config.Volumes && image.docker.Config.Volumes.length > 0) { + if (app.volumesEnabled && image.docker.Config.Volumes && image.docker.Config.Volumes.length > 0) { _.each(image.docker.Config.Volumes, function (vol) { - binds.push('/var/lib/docker/binds/' + app.name + vol.Path + ':' + vol.Path); + if (vol.Path && vol.Path.length && vol.Path[0] === '/') { + vol.Path = vol.Path.substr(1); + } + binds.push([Util.getHomePath(), 'Kitematic', app.name, vol.Path].join('/') + ':' + vol.Path); }); } - // Start the container - container.start({ - PublishAllPorts: true, + + var startOpts = { Binds: binds - }, function (err) { + }; + + if (app.docker && app.docker.NetworkSettings.Ports) { + startOpts.PortBindings = app.docker.NetworkSettings.Ports; + } else { + startOpts.PublishAllPorts = true; + } + + console.log(startOpts); + container.start(startOpts, function (err) { if (err) { callback(err, null); return; } console.log('Started container: ' + container.id); - Util.refreshDNS(app, function (err) { - if (err) { - console.error(err); - } - callback(null, container); - }); + callback(null, container); }); }); }; @@ -153,19 +170,6 @@ Docker.stopContainer = function (containerId, callback) { }); }; -Docker.restartContainer = function (containerId, callback) { - var container = Docker.client().getContainer(containerId); - container.restart(function (err) { - if (err) { - console.log(err); - callback(err); - return; - } - console.log('Restarted container: ' + containerId); - callback(null); - }); -}; - var convertVolumeObjToArray = function (obj) { var result = []; if (obj !== null && typeof obj === 'object') { @@ -180,37 +184,24 @@ var convertVolumeObjToArray = function (obj) { }; Docker.getImageData = function (imageId, callback) { - Docker.client().listImages({all: false}, function (err, images) { + var image = Docker.client().getImage(imageId); + image.inspect(function (err, data) { if (err) { callback(err, null); - } else { - var dockerImage = _.find(images, function (image) { - return image.Id === imageId; - }); - var image = Docker.client().getImage(imageId); - image.inspect(function (err, data) { - if (err) { - callback(err, null); - } else { - if (data.Config && data.Config.Volumes) { - data.Config.Volumes = convertVolumeObjToArray(data.Config.Volumes); - } - if (data.ContainerConfig && data.ContainerConfig.Volumes) { - data.ContainerConfig.Volumes = convertVolumeObjToArray(data.ContainerConfig.Volumes); - } - if (!dockerImage) { - callback(null, data); - } else { - callback(null, _.extend(dockerImage, data)); - } - } - }); + return; } + if (data.Config && data.Config.Volumes) { + data.Config.Volumes = convertVolumeObjToArray(data.Config.Volumes); + } + if (data.ContainerConfig && data.ContainerConfig.Volumes) { + data.ContainerConfig.Volumes = convertVolumeObjToArray(data.ContainerConfig.Volumes); + } + callback(null, data); }); }; -Docker.listImages = function (callback) { - Docker.client().listImages({all: false}, function (err, images) { +Docker.listImages = function (opts, callback) { + Docker.client().listImages(opts, function (err, images) { if (err) { callback(err, null); } else { @@ -220,7 +211,7 @@ Docker.listImages = function (callback) { if (err) { cb(err, null); } else { - cb(null, data); + cb(null, _.extend(image, data)); } }); }; @@ -244,238 +235,3 @@ Docker.removeImage = function (imageId, callback) { callback(null); }); }; - -Docker.removeBindFolder = function (name, callback) { - exec(Boot2Docker.command() + ' ssh "sudo rm -rf /var/lib/docker/binds/' + name + '"', function (err, stdout) { - callback(err, stdout); - }); -}; - -Docker.defaultContainerOptions = function () { - return [ - { - Image: 'kite-dns', - name: 'kite-dns', - PortBindings: {'53/udp': [{ 'HostPort': '53', 'HostIp': '172.17.42.1' }]}, - Binds: ['/var/run/docker.sock:/tmp/docker.sock'] - } - ]; -}; - -Docker.defaultContainerNames = Docker.defaultContainerOptions().map(function (container) { - return container.name; -}); - -Docker.checkDefaultImages = function (callback) { - var defaultNames = Docker.defaultContainerNames; - async.each(defaultNames, function (name, innerCallback) { - var image = Docker.client().getImage(name); - image.inspect(function (err) { - if (err) { - if (err.reason === 'no such image') { - innerCallback('no such image'); - } else { - innerCallback(err); - } - } else { - innerCallback(); - } - }); - }, function (err) { - if (err) { - callback(err); - } else { - callback(); - } - }); -}; - -Docker.resolveDefaultImages = function () { - async.each(Docker.defaultContainerNames, function (name, innerCallback) { - var image = Docker.client().getImage(name); - image.inspect(function (err) { - if (err) { - if (err.reason === 'no such image') { - Docker.client().loadImage(path.join(Util.getBinDir(), Docker.DEFAULT_IMAGES_FILENAME), {}, function (err) { - if (err) { - innerCallback(err); - return; - } else { - innerCallback(); - } - }); - } else { - innerCallback(err); - } - } else { - innerCallback(); - } - }); - }); -}; - -Docker.checkDefaultContainers = function(callback) { - async.each(Docker.defaultContainerNames, function (name, innerCallback) { - var container = Docker.client().getContainer(name); - container.inspect(function (err, data) { - if (err) { - innerCallback(err); - } else { - if (data && data.State && data.State.Running) { - innerCallback(null); - } else { - innerCallback('Not running'); - } - } - }); - }, function (err) { - if (err) { - callback(err); - } else { - callback(); - } - }); -}; - -Docker.resolveDefaultContainers = function (callback) { - Docker.killAndRemoveContainers(Docker.defaultContainerNames, function (err) { - if (err) { - callback(err); - return; - } - Docker.upContainers(Docker.defaultContainerOptions(), function (err) { - callback(err); - }); - }); -}; - -Docker.reloadDefaultContainers = function (callback) { - console.log('Reloading default containers.'); - var ready = false; - async.until(function () { - return ready; - }, function (callback) { - Docker.client().listContainers(function (err) { - if (!err) { - ready = true; - } - callback(); - }); - }, function () { - console.log('Removing old Kitematic default containers.'); - Docker.killAndRemoveContainers(Docker.defaultContainerNames, function (err) { - console.log('Removed old Kitematic default containers.'); - if (err) { - console.log('Removing old Kitematic default containers ERROR.'); - callback(err); - return; - } - console.log('Loading new Kitematic default images.'); - Docker.client().loadImage(path.join(Util.getResourceDir(), Docker.DEFAULT_IMAGES_FILENAME), {}, function (err) { - if (err) { - callback(err); - return; - } - console.log('Starting new Kitematic default containers.'); - Docker.upContainers(Docker.defaultContainerOptions(), function (err) { - callback(err); - }); - }); - }); - }); -}; - -Docker.upContainers = function (optionsList, callback) { - var createDefaultContainer = function (options, innerCallback) { - Docker.client().createContainer(options, function (err, container) { - if (err) { - innerCallback(err); - return; - } - container.start({ - PublishAllPorts: true, - PortBindings: options.PortBindings, - Binds: options.Binds - }, function (err) { - innerCallback(err); - }); - }); - }; - - async.each(optionsList, function (options, innerCallback) { - var container = Docker.client().getContainer(options.name); - container.inspect(function (err, data) { - if (err) { - if (err.reason.indexOf('no such container') !== -1) { - createDefaultContainer(options, function (err) { - innerCallback(err); - }); - } else { - innerCallback(err); - } - } else { - if (data && !data.State.Running) { - container.start(function (err) { - innerCallback(err); - }); - } else { - innerCallback(); - } - } - }); - }, function (err) { - callback(err); - }); -}; - -Docker.removeImages = function (names, callback) { - async.each(names, function (name, innerCallback) { - var image = Docker.client().getImage(name); - image.remove(function (err) { - if (err) { - console.log('remove image error'); - console.log(err); - if (err.reason === 'no such image') { - innerCallback(); - } else { - innerCallback(err); - } - } else { - innerCallback(); - } - }); - }, function (err) { - callback(err); - }); -}; - -Docker.killAndRemoveContainers = function (names, callback) { - async.each(names, function (name, innerCallback) { - var container = Docker.client().getContainer(name); - container.inspect(function (err, data) { - if (err) { - innerCallback(); - return; - } - if (data.State.Running) { - // Kill it - container.kill(function (err) { - if (err) { - innerCallback(err); - } else { - // Remove it - container.remove(function (err) { - innerCallback(err); - }); - } - }); - } else { - container.remove(function (err) { - innerCallback(err); - }); - } - }); - }, function (err) { - callback(err); - }); -}; diff --git a/meteor/client/lib/form-schemas.js b/meteor/client/lib/form-schemas.js index ea165b9b54..f96c5f1904 100644 --- a/meteor/client/lib/form-schemas.js +++ b/meteor/client/lib/form-schemas.js @@ -2,11 +2,11 @@ FormSchema = { formCreateApp: { name: { - label: 'app name', + label: 'container name', required: true, transforms: ['clean', 'slugify'], messages: { - 'uniqueAppName': "This app name is already being used." + 'uniqueAppName': "This container name is already being used." }, rules: { uniqueAppName: true diff --git a/meteor/client/lib/imageutil.js b/meteor/client/lib/imageutil.js index bc6f331d18..24b9e62e39 100644 --- a/meteor/client/lib/imageutil.js +++ b/meteor/client/lib/imageutil.js @@ -3,18 +3,10 @@ var convert = new Convert(); var exec = require('exec'); var path = require('path'); var fs = require('fs'); +var async = require('async'); ImageUtil = {}; -var createTarFile = function (image, callback) { - var TAR_PATH = path.join(Util.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 getFromImage = function (dockerfile) { var patternString = "(FROM)(.*)"; var regex = new RegExp(patternString, "g"); @@ -54,23 +46,8 @@ ImageUtil.getMetaData = function (directory) { return kiteJSON; }; -ImageUtil.saveFolder = function (directory, imageId, callback) { - var destinationPath = path.join(Util.KITE_IMAGES_PATH, imageId); - if (!fs.existsSync(destinationPath)) { - fs.mkdirSync(destinationPath, function (err) { - if (err) { callback(err); return; } - }); - Util.copyFolder(directory, destinationPath, function (err) { - if (err) { callback(err); return; } - console.log('Copied image folder for: ' + imageId); - callback(null); - }); - } -}; - ImageUtil.rebuildHelper = function (image, callback) { - Util.deleteFolder(image.path); - var imageMetaData = ImageUtil.getMetaData(image.originPath); + var imageMetaData = ImageUtil.getMetaData(image.path); if (imageMetaData.logo) { Images.update(image._id, { $set: { @@ -91,14 +68,11 @@ ImageUtil.rebuildHelper = function (image, callback) { } }); image = Images.findOne(image._id); - ImageUtil.saveFolder(image.originPath, image._id, function (err) { - if (err) { console.error(err); } - ImageUtil.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) { - if (err) { callback(err, null); return; } - ImageUtil.build(image, function (err) { - if (err) { console.error(err); } - callback(null, null); - }); + ImageUtil.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) { + if (err) { callback(err, null); return; } + ImageUtil.build(image, function (err) { + if (err) { console.error(err); } + callback(null, null); }); }); }; @@ -146,7 +120,6 @@ ImageUtil.pull = function (dockerfile, imageId, callback) { console.log('From image: ' + fromImage); var installedImage = null; Docker.getImageData(imageId, function (err, data) { - if (err) { console.error(err); } installedImage = data; if (fromImage && !installedImage) { Images.update(imageId, { @@ -191,15 +164,15 @@ ImageUtil.pull = function (dockerfile, imageId, callback) { }; ImageUtil.build = function (image, callback) { - createTarFile(image, function (err, tarFilePath) { + Util.createTarFile(image.path, path.join(Util.KITE_TAR_PATH, image._id + '.tar'), function (err, tarFilePath) { if (err) { console.error(err); } Images.update(image._id, { $set: { buildLogs: [] } }); - Docker.client().buildImage(tarFilePath, {t: image.meta.name + ':' + image.meta.version}, function (err, response) { - if (err) { callback(err); } + Docker.client().buildImage(tarFilePath, {forcerm: true, t: image.meta.name + ':' + image.meta.version}, function (err, response) { + if (err) { callback(err); return; } console.log('Building Docker image...'); response.setEncoding('utf8'); response.on('data', function (data) { @@ -211,7 +184,7 @@ ImageUtil.build = function (image, callback) { } }); } catch (e) { - console.error(e); + // Ignore misc conversion errors } }); response.on('end', function () { @@ -224,7 +197,6 @@ ImageUtil.build = function (image, callback) { } var imageData = null; Docker.getImageData(image.meta.name + ':' + image.meta.version, function (err, data) { - console.log(data); if (err) { console.error(err); Images.update(image._id, { @@ -265,88 +237,98 @@ ImageUtil.remove = function (imageId) { if (err) { console.error(err); } }); } - try { - Util.deleteFolder(image.path); - } catch (e) { - console.error(e); - } - Sync.removeAppWatcher(imageId); }; -ImageUtil.sync = function () { - Docker.listImages(function (err, dockerImages) { +ImageUtil.sync = function (callback) { + Docker.listImages({all: 0}, function (err, dockerImages) { if (err) { - console.error(err); - } else { - var images = Images.find({}).fetch(); - _.each(images, function (image) { - var image = Images.findOne(image._id); - if (image && image.docker && image.docker.Id) { - var duplicateImages = Images.find({'docker.Id': image.docker.Id, _id: {$ne: image._id}}).fetch(); - _.each(duplicateImages, function (duplicateImage) { - Images.remove(duplicateImage._id); - }); - var imageData = _.find(dockerImages, function (dockerImage) { - return dockerImage.Id === image.docker.Id; - }); - if (imageData && imageData.RepoTags) { - Images.update(image._id, { - $set: { - tags: imageData.RepoTags - } - }); - } - Docker.getImageData(image.docker.Id, function (err, data) { - Images.update(image._id, { - $set: { - docker: data - } - }) - }); - } - }); - var dockerIds = _.map(images, function (image) { - if (image.docker && image.docker.Id) { - return image.docker.Id; - } - }); - var imageIds = _.map(dockerImages, function (image) { - return image.Id; - }); - var diffImages = _.difference(dockerIds, imageIds); - _.each(diffImages, function (imageId) { - var image = Images.findOne({'docker.Id': imageId}); - if (image && image.status !== 'BUILDING') { - ImageUtil.remove(image._id); - } - }); - var diffDockerImages = _.reject(dockerImages, function (image) { - return _.contains(dockerIds, image.Id); - }); - _.each(diffDockerImages, function (image) { - var repoTag = _.first(image.RepoTags); - var repoTagTokens = repoTag.split(':'); - var name = repoTagTokens[0]; - var version = repoTagTokens[1]; - var buildingImage = _.find(images, function (image) { - return image.status === 'BUILDING' && image.meta.name === name && image.meta.version === version; - }); - if (!buildingImage && name !== '' && version !== '' && name !== 'kite-dns') { - var imageObj = { - status: 'READY', - docker: image, - buildLogs: [], - createdAt: new Date(), - tags: image.RepoTags, - meta: { - name: name, - version: version - } - }; - console.log(imageObj); - Images.insert(imageObj); - } - }); + callback(err); + return; } + var images = Images.find({}).fetch(); + + // Delete missing GUI images + var kitematicIds = _.map(images, function (image) { + if (image.docker && image.docker.Id) { + return image.docker.Id; + } + }); + var daemonIds = _.map(dockerImages, function (image) { + return image.Id; + }); + var diffImages = _.difference(kitematicIds, daemonIds); + _.each(diffImages, function (imageId) { + var image = Images.findOne({'docker.Id': imageId}); + if (image && image.status !== 'BUILDING') { + Images.remove(image._id); + } + }); + + // Add missing Daemon images + var diffDockerImages = _.reject(dockerImages, function (image) { + return _.contains(kitematicIds, image.Id); + }); + _.each(diffDockerImages, function (image) { + if (!image.RepoTags || _.isEmpty(image.Config.ExposedPorts)) { + return; + } + + var meta = {}; + var repoTag = _.first(image.RepoTags); + var repoTagTokens = repoTag.split(':'); + var name = repoTagTokens[0]; + var version = repoTagTokens[1]; + meta = { + name: name, + version: version + }; + var buildingImage = _.find(images, function (image) { + return image.status === 'BUILDING' && image.meta.name === name && image.meta.version === version; + }); + if (!buildingImage) { + var imageObj = { + status: 'READY', + docker: image, + buildLogs: [], + createdAt: new Date(), + tags: image.RepoTags, + meta: meta + }; + Images.insert(imageObj); + } + }); + + async.each(images, function (image, callback) { + var image = Images.findOne(image._id); + if (image && image.docker && image.docker.Id) { + var duplicateImages = Images.find({'docker.Id': image.docker.Id, _id: {$ne: image._id}}).fetch(); + _.each(duplicateImages, function (duplicateImage) { + Images.remove(duplicateImage._id); + }); + var imageData = _.find(dockerImages, function (dockerImage) { + return dockerImage.Id === image.docker.Id; + }); + if (imageData && imageData.RepoTags) { + Images.update(image._id, { + $set: { + tags: imageData.RepoTags + } + }); + } + Docker.getImageData(image.docker.Id, function (err, data) { + if (err) {callback(err); return;} + Images.update(image._id, { + $set: { + docker: data + } + }); + callback(); + }); + } else { + callback(); + } + }, function (err) { + callback(err); + }); }); }; diff --git a/meteor/client/lib/installer.js b/meteor/client/lib/installer.js deleted file mode 100644 index 3bff06d94a..0000000000 --- a/meteor/client/lib/installer.js +++ /dev/null @@ -1,206 +0,0 @@ -var async = require('async'); -var fs = require('fs'); -var path = require('path'); -var remote = require('remote'); -var app = remote.require('app'); - -Installer = {}; - -Installer.CURRENT_VERSION = app.getVersion(); -Installer.BASE_URL = 'https://s3.amazonaws.com/kite-installer/'; - -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, progressCallback) { - var installed = VirtualBox.installed(); - if (!installed) { - Util.downloadFile(Installer.BASE_URL + VirtualBox.INSTALLER_FILENAME, path.join(Util.getResourceDir(), VirtualBox.INSTALLER_FILENAME), VirtualBox.INSTALLER_CHECKSUM, function (err) { - if (err) {callback(err); return;} - VirtualBox.install(function (err) { - if (!VirtualBox.installed()) { - callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.'); - } else { - callback(err); - } - }); - }, function (progress) { - progressCallback(progress); - }); - } else { - VirtualBox.version(function (err, installedVersion) { - if (err) {callback(err); return;} - if (Util.compareVersions(installedVersion, VirtualBox.REQUIRED_VERSION) < 0) { - // Download a newer version of Virtualbox - Util.downloadFile(Installer.BASE_URL + VirtualBox.INSTALLER_FILENAME, path.join(Util.getResourceDir(), VirtualBox.INSTALLER_FILENAME), VirtualBox.INSTALLER_CHECKSUM, function (err) { - if (err) {callback(err); return;} - VirtualBox.install(function (err) { - if (err) {callback(err); return;} - VirtualBox.version(function (err, installedVersion) { - if (err) {callback(err); return;} - if (Util.compareVersions(installedVersion, VirtualBox.REQUIRED_VERSION) < 0) { - callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.'); - } else { - callback(err); - } - }); - }); - }, function (progress) { - progressCallback(progress); - }); - } else { - callback(); - } - }); - } - }, - pastMessage: 'VirtualBox Installed', - message: 'Downloading & Installing VirtualBox', - futureMessage: 'Download & Install VirtualBox if necessary' - }, - - // Initialize Boot2Docker if necessary. - { - run: function (callback) { - Boot2Docker.exists(function (err, exists) { - if (err) { callback(err); return; } - if (!exists) { - var vmFilesPath = path.join(Util.getHomePath(), 'VirtualBox\ VMs', 'kitematic-vm'); - if (fs.existsSync(vmFilesPath)) { - Util.deleteFolder(vmFilesPath); - } - Boot2Docker.init(function (err) { - callback(err); - }); - } else { - if (!Boot2Docker.sshKeyExists()) { - callback('Boot2Docker SSH key doesn\'t exist. Fix by deleting the existing Boot2Docker VM and re-run the installer. This usually occurs because an old version of Boot2Docker is installed.'); - } else { - Boot2Docker.stop(function(err) { - Boot2Docker.upgrade(function (err) { - callback(err); - }); - }); - } - } - }); - }, - pastMessage: 'Setup the Kitematic VM (if required)', - message: 'Setting up the Kitematic VM', - futureMessage: 'Set up the Kitematic VM (if required)' - }, - - { - run: function (callback) { - VirtualBox.addCustomHostAdapter('kitematic-vm', function (err, ifname) { - callback(err); - }); - }, - pastMessage: 'Added custom host adapter to the Kitematic VM', - message: 'Adding custom host adapter to the Kitematic VM', - futureMessage: 'Add custom host adapter to the Kitematic 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) { - if (err) { - callback(err); - return; - } - Boot2Docker.setIp('eth1', Boot2Docker.REQUIRED_IP, function(err) { - callback(err); - }); - }); - } else { - Boot2Docker.setIp('eth1', Boot2Docker.REQUIRED_IP, 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' - }, - - { - run: function (callback) { - VirtualBox.setupRouting('kitematic-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, progressCallback) { - Util.downloadFile(Installer.BASE_URL + Docker.DEFAULT_IMAGES_FILENAME, path.join(Util.getResourceDir(), Docker.DEFAULT_IMAGES_FILENAME), Docker.DEFAULT_IMAGES_CHECKSUM, function (err) { - Docker.reloadDefaultContainers(function (err) { - callback(err); - }); - }, function (progress) { - progressCallback(progress); - }); - }, - pastMessage: 'Set up the default Kitematic images.', - 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) { - Session.set('installing', true); - 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); - Session.set('currentInstallStepProgress', 0); - step.run(function (err) { - if (err) { - callback(err); - } else { - currentStep += 1; - Session.set('currentInstallStep', currentStep); - callback(); - } - }, function (progress) { - Session.set('currentInstallStepProgress', progress); - }); - }, 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 - Session.set('installing', false); - console.log('Setup finished.'); - callback(); - } - }); -}; diff --git a/meteor/client/lib/ipc.js b/meteor/client/lib/ipc.js new file mode 100644 index 0000000000..fb655ad56f --- /dev/null +++ b/meteor/client/lib/ipc.js @@ -0,0 +1,15 @@ +var ipc = require('ipc'); + +// Listen for auto updates +ipc.on('notify', function (message) { + if (message === 'window:update-available') { + Session.set('updateAvailable', true); + } + if (message === 'application:quit') { + VirtualBox.saveVMState('boot2docker-vm', function (err) { + if (err) { + console.log(err); + } + }); + } +}); \ No newline at end of file diff --git a/meteor/client/lib/router.js b/meteor/client/lib/router.js index c3bbef1d6b..47444c424a 100755 --- a/meteor/client/lib/router.js +++ b/meteor/client/lib/router.js @@ -4,76 +4,60 @@ Router.configure({ var setting = Settings.findOne({}); if (setting && setting.tracking) { var currentPath = Router.current().path; - console.log(currentPath); ga('send', 'pageview', currentPath); } - } -}); - -SetupController = RouteController.extend({ - layoutTemplate: 'setup_layout', - waitOn: function () { - return [Meteor.subscribe('apps'), Meteor.subscribe('images'), Meteor.subscribe('installs'), Meteor.subscribe('settings')]; + this.next(); } }); DashboardController = RouteController.extend({ - layoutTemplate: 'dashboard_layout', + layoutTemplate: 'dashboardLayout', waitOn: function () { - return [Meteor.subscribe('apps'), Meteor.subscribe('images'), Meteor.subscribe('installs'), Meteor.subscribe('settings')]; + return [Meteor.subscribe('apps'), Meteor.subscribe('images'), Meteor.subscribe('settings')]; } }); AppController = DashboardController.extend({ - layoutTemplate: 'dashboard_apps_layout', + layoutTemplate: 'dashboardAppsLayout', data: function () { return Apps.findOne({name: this.params.name}); } }); ImageController = DashboardController.extend({ - layoutTemplate: 'dashboard_images_layout', + layoutTemplate: 'dashboardImagesLayout', data: function () { return Images.findOne({_id: this.params.id}); } }); Router.map(function () { - - this.route('setup_intro', { - path: '/setup/intro', - controller: 'SetupController', - onBeforeAction: function () { - Session.set('installing', true); - } - }); - - this.route('setup_install', { - path: '/setup/install', - controller: 'SetupController' - }); - - this.route('setup_finish', { - path: '/setup/finish', - controller: 'SetupController' - }); - - this.route('setup', { + this.route('intro', { path: '/', - controller: 'SetupController', + waitOn: function () { + return [Meteor.subscribe('apps'), Meteor.subscribe('images'), Meteor.subscribe('settings')]; + }, action: function () { if (this.ready()) { - if (!Installer.isUpToDate()) { - if (!Installs.findOne()) { - console.log('No installs detected, running installer again.'); + this.render(); + Setup.run(function (err) { + if (err) { + console.log('Setup failed.'); + console.log(err); } else { - // There's an install but it's lower than the current version, re-run as an 'update'. - Session.set('isUpdating', true); + var settings = Settings.findOne(); + if (!settings) { + Settings.insert({tracking: true}); + } + startUpdatingBoot2DockerUtilization(); + startSyncingAppState(); + if (Apps.findOne()) { + Router.go('dashboard_apps'); + } else { + Router.go('dashboard_images'); + } } - this.redirect('/setup/intro'); - } else { - this.redirect('/apps'); - } + }); } } }); diff --git a/meteor/client/lib/setup.js b/meteor/client/lib/setup.js new file mode 100644 index 0000000000..bda23abf52 --- /dev/null +++ b/meteor/client/lib/setup.js @@ -0,0 +1,173 @@ +var async = require('async'); +var fs = require('fs'); +var path = require('path'); +var remote = require('remote'); +var app = remote.require('app'); + +Setup = {}; + +Setup.CURRENT_VERSION = app.getVersion(); +Setup.BASE_URL = 'https://s3.amazonaws.com/kite-installer/'; + +Setup.isUpToDate = function () { + return !!Installs.findOne({version: Setup.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. + * - message: Message to show while step is running + */ +Setup.steps = [ + { + run: function (callback, progressCallback) { + var installed = VirtualBox.installed(); + if (!installed) { + Util.downloadFile(Setup.BASE_URL + VirtualBox.INSTALLER_FILENAME, path.join(Util.getResourceDir(), VirtualBox.INSTALLER_FILENAME), VirtualBox.INSTALLER_CHECKSUM, function (err) { + if (err) {callback(err); return;} + VirtualBox.install(function (err) { + if (!VirtualBox.installed()) { + callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.'); + } else { + callback(err); + } + }); + }, function (progress) { + progressCallback(progress); + }); + } else { + VirtualBox.version(function (err, installedVersion) { + if (err) {callback(err); return;} + if (Util.compareVersions(installedVersion, VirtualBox.REQUIRED_VERSION) < 0) { + // Download a newer version of Virtualbox + Util.downloadFile(Setup.BASE_URL + VirtualBox.INSTALLER_FILENAME, path.join(Util.getResourceDir(), VirtualBox.INSTALLER_FILENAME), VirtualBox.INSTALLER_CHECKSUM, function (err) { + if (err) {callback(err); return;} + VirtualBox.killAllProcesses(function (err) { + if (err) {callback(err); return;} + VirtualBox.install(function (err) { + if (err) {callback(err); return;} + VirtualBox.version(function (err, installedVersion) { + if (err) {callback(err); return;} + if (Util.compareVersions(installedVersion, VirtualBox.REQUIRED_VERSION) < 0) { + callback('VirtualBox could not be installed. The installation either failed or was cancelled. Please try closing all VirtualBox instances and try again.'); + } else { + callback(err); + } + }); + }); + }); + }, function (progress) { + progressCallback(progress); + }); + } else { + callback(); + } + }); + } + }, + message: 'Downloading VirtualBox...' + }, + { + run: function (callback) { + VirtualBox.shutdownVM('kitematic-vm', function (err, removed) { + if (err) { + console.log(err); + } + callback(); + }); + }, + message: 'Cleaning up existing Docker VM...' + }, + + // Initialize Boot2Docker if necessary. + { + run: function (callback) { + Boot2Docker.exists(function (err, exists) { + if (err) { callback(err); return; } + if (!exists) { + Boot2Docker.init(function (err) { + callback(err); + }); + } else { + if (!Boot2Docker.sshKeyExists()) { + callback('Boot2Docker SSH key doesn\'t exist. Fix by removing the existing Boot2Docker VM and re-run the installer. This usually occurs because an old version of Boot2Docker is installed.'); + } else { + Boot2Docker.vmUpToDate(function (err, upToDate) { + if (err) {callback(err); return;} + if (!upToDate) { + Boot2Docker.stop(function(err) { + Boot2Docker.upgrade(function (err) { + callback(err); + }); + }); + } else { + callback(); + } + }); + } + } + }); + }, + message: 'Setting up the Docker VM...' + }, + { + run: function (callback) { + Boot2Docker.waitWhileStatus('saving', function (err) { + Boot2Docker.status(function (err, status) { + if (err) {callback(err); return;} + if (status !== 'running') { + Boot2Docker.start(function (err) { + callback(err); + }); + } else { + callback(); + } + }); + }); + }, + message: 'Starting the Docker VM...' + }, + { + run: function (callback) { + Boot2Docker.ip(function (err, ip) { + if (err) {callback(err); return;} + console.log('Setting host IP to: ' + ip); + Docker.setHost(ip); + callback(err); + }); + }, + message: 'Detecting Docker VM...' + } +]; + +Setup.run = function (callback) { + var currentStep = 0; + Session.set('currentSetupStepMessage', currentStep); + async.eachSeries(this.steps, function (step, callback) { + console.log('Performing step ' + currentStep); + Session.set('currentSetupStepProgress', 0); + Session.set('currentSetupStepMessage', step.message); + step.run(function (err) { + if (err) { + callback(err); + } else { + currentStep += 1; + callback(); + } + }, function (progress) { + Session.set('currentSetupStepProgress', progress); + }); + }, function (err) { + if (err) { + // if any of the steps fail + console.log('Kitematic setup failed at step ' + currentStep); + console.log(err); + Session.set('currentSetupFailedError', err); + callback(err); + } else { + // Setup Finished + console.log('Setup finished.'); + callback(); + } + }); +}; diff --git a/meteor/client/lib/startup.js b/meteor/client/lib/startup.js index d0b9b6ba8e..c53faaee3e 100644 --- a/meteor/client/lib/startup.js +++ b/meteor/client/lib/startup.js @@ -1,4 +1,5 @@ var fs = require('fs'); + Meteor.startup(function () { console.log('Kitematic started.'); if (!fs.existsSync(Util.KITE_PATH)) { @@ -16,4 +17,11 @@ Meteor.startup(function () { if (!fs.existsSync(Util.getResourceDir())) { fs.mkdirSync(Util.getResourceDir()); } + + Boot2Docker.ip(function (err, ip) { + if (!err) { + console.log('Setting host IP to: ' + ip); + Docker.setHost(ip); + } + }); }); diff --git a/meteor/client/lib/sync.js b/meteor/client/lib/sync.js deleted file mode 100644 index 44e053f8d8..0000000000 --- a/meteor/client/lib/sync.js +++ /dev/null @@ -1,124 +0,0 @@ -var chokidar = require('chokidar'); -var path = require('path'); -var fs = require('fs'); -var child_process = require('child_process'); -var exec = require('exec'); - -Sync = {}; -Sync.watchers = {}; - -Sync.removeAppWatcher = function (id) { - if (Sync.watchers[id]) { - Sync.watchers[id].watcher.close(); - delete Sync.watchers[id]; - } -}; - -Sync.addAppWatcher = function (app) { - Sync.removeAppWatcher(app._id); - var appPath = 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/}); - var syncing = false; - var willSyncAgain = false; - - var syncFunc = function (event, changedPath) { - if (syncing) { - willSyncAgain = true; - return; - } - 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(Util.getBinDir(), 'unison'); - var args = [ - cmd, - vmPath, - appPath, - '-prefer', - vmPath, - '-servercmd', - 'sudo\ unison', - '-batch', - '-log=false', - '-confirmbigdel=false', - '-ignore', - 'Name\ {*.tmp,*.unison,*.swp,*.pyc,.DS_Store}', - '-auto', - '-sshargs', - '-o\ UserKnownHostsFile=/dev/null\ -o\ StrictHostKeyChecking=no\ -o PreferredAuthentications=publickey\ -i\ ' + path.join(Util.getHomePath(), '.ssh/id_boot2docker') - ]; - - if (!fs.existsSync(appPath)) { - args.push('-ignorearchives'); - console.log('Created Kite ' + app.name + ' directory.'); - fs.mkdirSync(appPath, function (err) { - if (err) { throw err; } - }); - } - - exec(args, function (err, out, code) { - var results = null; - var location = null; - try { - if (err.indexOf('the archives are locked.') !== -1) { - results = errorPattern.exec(err); - location = results[1].replace(' ', '\\ '); - exec('/bin/rm -rf ' + location, function () { - console.log('Removed unison file.'); - console.log(location); - }); - } - if (err.indexOf('The archive file is missing on some hosts') !== -1) { - results = archiveErrorPattern.exec(err); - location = results[1].replace(' ', '\\ '); - 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); - } - syncing = false; - if (willSyncAgain) { - syncFunc(); - willSyncAgain = false; - } - }); - }; - - Sync.watchers[app._id] = { - watcher: watcher, - sync: syncFunc - }; - - // do a sync - syncFunc(); - watcher.on('all', syncFunc); -}; - -Sync.resolveWatchers = function (callback) { - var apps = Apps.find({}).fetch(); - var ids = _.map(apps, function(app) { - return app._id; - }); - var watcherKeys = _.keys(Sync.watchers); - var toAdd = _.difference(ids, watcherKeys); - var toRemove = _.difference(watcherKeys, ids); - - _.each(toAdd, function (id) { - Sync.addAppWatcher(Apps.findOne(id), function () {}); - }); - - _.each(toRemove, function (id) { - Sync.removeAppWatcher(id); - }); - - // Run a sync for 'pulling' changes in the volumes. - _.each(Sync.watchers, function (watcher) { - watcher.sync(); - }); - - callback(); -}; diff --git a/meteor/client/lib/util.js b/meteor/client/lib/util.js index 0f1da75816..a1edeb2bc2 100755 --- a/meteor/client/lib/util.js +++ b/meteor/client/lib/util.js @@ -9,20 +9,6 @@ ncp.limit = 16; Util = {}; -Util.refreshDNS = function (app, callback) { - // Use dig to refresh the DNS - exec('/usr/bin/dig ' + app.name + '.kite @172.17.42.1', function (err, stdout, stderr) { - console.log(err); - console.log(stdout); - console.log(stderr); - if (err) { - callback(err); - } else { - callback(null); - } - }); -}; - Util.getHomePath = function () { return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; }; @@ -91,17 +77,24 @@ Util.copyVolumes = function (directory, appName, callback) { } }; +Util.createTarFile = function (sourcePath, destinationFile, callback) { + exec(['tar', 'czf', destinationFile, '-C', sourcePath, '.'], function (err) { + if (err) {callback(err, null); return;} + console.log('Created tar file: ' + destinationFile); + callback(null, destinationFile); + }); +}; + Util.hasDockerfile = function (directory) { return fs.existsSync(path.join(directory, 'Dockerfile')); }; Util.openTerminal = function (command) { - var terminalCmd = path.join(Util.getBinDir(), 'terminal') + ' ' + command; - var exec = require('child_process').exec; - exec(terminalCmd, function (err, stdout) { - console.log(stdout); - if (err) { - console.log(err); + var cmd = [path.join(Util.getBinDir(), 'terminal')]; + cmd.push.apply(cmd, command); + exec(cmd, function (stderr, stdout, code) { + if (code) { + console.log(stderr); } }); }; @@ -142,25 +135,6 @@ Util.downloadFile = function (url, filename, checksum, callback, progressCallbac * @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, @@ -217,7 +191,6 @@ trackLink = function (trackLabel) { var setting = Settings.findOne({}); if (setting && setting.tracking) { if (trackLabel) { - console.log(trackLabel); ga('send', 'event', 'link', 'click', trackLabel); } } diff --git a/meteor/client/lib/virtualbox.js b/meteor/client/lib/virtualbox.js index 6c0b6a1c30..17ed9066a2 100644 --- a/meteor/client/lib/virtualbox.js +++ b/meteor/client/lib/virtualbox.js @@ -1,6 +1,7 @@ var fs = require('fs'); var exec = require('exec'); var path = require('path'); +var async = require('async'); VirtualBox = {}; @@ -9,28 +10,22 @@ VirtualBox.INCLUDED_VERSION = '4.3.18'; VirtualBox.INSTALLER_FILENAME = 'virtualbox-4.3.18.pkg'; VirtualBox.INSTALLER_CHECKSUM = '5836c94481c460c648b9216386591a2915293ac86b9bb6c57746637796af6af2'; // Sha256 Checksum -// 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') && fs.existsSync('/Applications/VirtualBox.app/Contents/MacOS/VirtualBox'); }; VirtualBox.exec = function (command, callback) { - exec('/usr/bin/VBoxManage ' + command, function (error, stdout, stderr) { - callback(error, stdout, stderr); + exec(['/usr/bin/VBoxManage', command], function (stderr, stdout, code) { + callback(stderr, stdout, code); }); }; VirtualBox.install = function (callback) { // -W waits for the process to close before finishing. - exec('open -W ' + path.join(Util.getResourceDir(), this.INSTALLER_FILENAME).replace(' ', '\\ '), function (error, stdout, stderr) { - console.log(stdout); - console.log(stderr); - if (error) { - callback(error); + exec('open -W ' + path.join(Util.getResourceDir(), this.INSTALLER_FILENAME).replace(' ', '\\ '), function (stderr, stdout, code) { + if (code) { + callback(stderr); return; } callback(null); @@ -42,9 +37,9 @@ VirtualBox.version = function (callback) { callback('VirtualBox not installed.'); return; } - this.exec('-v', function (err, stdout, stderr) { - if (err) { - callback(err); + this.exec('-v', function (stderr, stdout, code) { + if (code) { + callback(stderr); return; } // Output is x.x.xryyyyyy @@ -57,106 +52,67 @@ VirtualBox.version = function (callback) { }); }; -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, {}); +VirtualBox.saveRunningVMs = function (callback) { + if (!this.installed()) { + callback('VirtualBox not installed.'); + return; + } + this.exec('list runningvms | sed -E \'s/.*\\{(.*)\\}/\\1/\' | xargs -L1 -I {} VBoxManage controlvm {} savestate', function (stderr, stdout, code) { + if (code) { + callback(stderr); } else { - var objs = {}; - _.each(matches, function (match) { - var pieces = match.split('='); - objs[pieces[0]] = pieces[1].replace(/"/g, ''); - }); - callback(null, objs); + 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) { +VirtualBox.killAllProcesses = function (callback) { + this.saveRunningVMs(function (err) { + if (err) {callback(err); return;} + exec('pkill VirtualBox', function (stderr, stdout, code) { + if (code) {callback(stderr); return;} + exec('pkill VBox', function (stderr, stdout, code) { + if (code) {callback(stderr); return;} + callback(); + }); + }); + }); +}; + +VirtualBox.vmState = function (name, callback) { + VirtualBox.exec('showvminfo ' + name + ' --machinereadable', function (stderr, stdout, code) { + if (code) { callback(stderr); return; } + var match = stdout.match(/VMState="(\w+)"/); + if (!match) { + callback('Could not parse VMState'); + return; + } + callback(null, match[1]); + }); +}; + +VirtualBox.shutdownVM = function (name, callback) { + VirtualBox.vmState(name, function (err, state) { + // No VM found + if (err) { callback(null, false); return; } + VirtualBox.exec('controlvm ' + name + ' acpipowerbutton', function (stderr, stdout, code) { + if (code) { callback(stderr, false); return; } + var state = null; + + async.until(function () { + return state === 'poweroff'; + }, function (callback) { + VirtualBox.vmState(name, function (err, newState) { if (err) { callback(err); return; } - callback(null, match[1]); + state = newState; + setTimeout(callback, 250); + }); + }, function (err) { + VirtualBox.exec('unregistervm ' + name + ' --delete', function (stderr, stdout, code) { + if (code) { callback(err); return; } + callback(); }); }); - } 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 + ' --nic2 hostonly --nictype2 virtio --cableconnected2 on --hostonlyadapter2 ' + 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); - }); - }); -}; +}; \ No newline at end of file diff --git a/meteor/client/main.js b/meteor/client/main.js index 4f04f15b31..c1b3fd6c18 100755 --- a/meteor/client/main.js +++ b/meteor/client/main.js @@ -1,11 +1,5 @@ try { moment = require('moment'); - // gui = require('nw.gui'); - // gui.App.clearCache(); - // win = gui.Window.get(); - // var nativeMenuBar = new gui.Menu({type: 'menubar'}); - // nativeMenuBar.createMacBuiltin('Kitematic'); - // win.menu = nativeMenuBar; } catch (e) { console.error(e); } @@ -75,90 +69,48 @@ Handlebars.registerHelper('displayTags', function (tags, delimiter) { } }); -var fixBoot2DockerVM = function (callback) { - Boot2Docker.check(function (err) { - if (err) { - Session.set('available', false); - Boot2Docker.resolve(function (err) { - if (err) { - callback(err); +updateBoot2DockerUtilization = function (callback) { + Boot2Docker.exists(function (err, exists) { + if (err) { callback(err); return; } + if (exists) { + Boot2Docker.state(function (err, state) { + if (err) { callback(err); return; } + Session.set('boot2dockerState', state); + if (state === 'running') { + Boot2Docker.stats(function (err, stats) { + if (err) { callback(err); return; } + if (stats.state !== 'poweroff' && stats.memory && stats.disk) { + Session.set('boot2dockerMemoryUsage', stats.memory); + Session.set('boot2dockerDiskUsage', stats.disk); + } + callback(); + }); } else { - Session.set('available', true); callback(); } }); - } else { - callback(); } }); }; -var fixDefaultImages = function (callback) { - Docker.checkDefaultImages(function (err) { - if (err) { - Session.set('available', false); - Docker.resolveDefaultImages(function (err) { - if (err) { - callback(); - } else { - Session.set('available', true); - callback(); - } - }); - } else { - Session.set('available', true); - callback(); - } +startUpdatingBoot2DockerUtilization = function () { + updateBoot2DockerUtilization(function (err) { + if (err) {console.log(err);} + Meteor.setTimeout(startUpdatingBoot2DockerUtilization, 2000); }); }; -var fixDefaultContainers = function (callback) { - Docker.checkDefaultContainers(function (err) { - if (err) { - Session.set('available', false); - Docker.resolveDefaultContainers(function (err) { - if (err) { - callback(err); - } else { - Session.set('available', true); - callback(); - } +startSyncingAppState = function () { + try { + ImageUtil.sync(function (err) { + if (err) {throw err;} + AppUtil.sync(function (err) { + if (err) {throw err;} + Meteor.setTimeout(startSyncingAppState, 2000); }); - } else { - Session.set('available', true); - callback(); - } - }); -}; - -Meteor.setInterval(function () { - if (!Session.get('installing')) { - 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); - } - }); - } - }); - } }); + } catch (err) { + console.log(err); + Meteor.setTimeout(startSyncingAppState, 2000); } -}, 5000); - -Meteor.setInterval(function () { - if (!Session.get('installing')) { - Sync.resolveWatchers(function () {}); - ImageUtil.sync(); - AppUtil.sync(); - AppUtil.recover(); - } -}, 5000); +}; \ No newline at end of file diff --git a/meteor/client/main.less b/meteor/client/main.less index 3558ad128d..68d5a05c6d 100755 --- a/meteor/client/main.less +++ b/meteor/client/main.less @@ -7,6 +7,6 @@ @import "stylesheets/widgets.import.less"; @import "stylesheets/mac.import.less"; @import "stylesheets/dashboard.import.less"; -@import "stylesheets/setup.import.less"; +@import "stylesheets/intro.import.less"; @import "stylesheets/spinner.import.less"; -@import "stylesheets/radial-progress.import.less"; +@import "stylesheets/radial.import.less"; diff --git a/meteor/client/stylesheets/dashboard.import.less b/meteor/client/stylesheets/dashboard.import.less index b97cd726f5..574162f4bc 100755 --- a/meteor/client/stylesheets/dashboard.import.less +++ b/meteor/client/stylesheets/dashboard.import.less @@ -20,6 +20,51 @@ .btn-icon { font-size: 20px; margin-left: 0.4em; + cursor: pointer; + } + .dropdown { + &.open { + .tooltip { + display: none !important; + } + } + } + .ports { + position: relative; + top: -2px; + .dropdown-menu { + min-width: 241px; + padding: 10px 15px 3px; + } + .btn-group { + top: -2px; + } + .btn-globe { + line-height: 22px; + padding: 0 5px; + .typcn { + font-size: 18px; + top: 2px; + position: relative; + } + } + .btn-ports { + .caret { + margin-left: 3px; + margin-right: 1px; + margin-top: -2px + } + } + .btn-caret { + padding-top: 3px; + padding-bottom: 3px; + border-width: 1px; + padding-left: 3px; + padding-right: 3px; + } + &.open + .tooltip { + display: none !important; + } } } &:hover { @@ -65,6 +110,28 @@ } } + .update-alert { + position: absolute; + bottom: 0; + right: 0; + margin-bottom: 0; + padding: 5px; + width: 100%; + text-align: right; + background: white; + font-size: 12px; + z-index: 2; + color: @text-color-less-light; + border-top: 1px solid @text-color-lightest; + box-shadow: 0 -1px 2px 0 rgba(0, 0, 0, 0.05); + + .update-text { + position: relative; + top: 2px; + right: 5px; + } + } + .dashboard-row { .make-row(); .dashboard-menu { @@ -139,8 +206,7 @@ } .content { .padded { - padding: 1.6em; - padding-top: 0; + padding: 0 1.6em 2.5em; } &.longer { min-height: @dashboard-content-height + @dashboard-header-height; @@ -217,24 +283,65 @@ } } - .env-var-pair { - .make-row(); - margin-bottom: 0.2em; - font-size: 12px; - .env-var-key { - .make-xs-column(5); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; + .form-env-vars { + label { + font-size: 13px; } - .env-var-value { - .make-xs-column(5); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; + input { + font-size: 13px; + padding: 5px 9px; } - .options { - .make-xs-column(2); + .env-var-pair { + .make-row(); + margin-bottom: 0.2em; + font-size: 13px; + .env-var-key { + .make-xs-column(5); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .env-var-value { + .make-xs-column(5); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .options { + .make-xs-column(2); + } + } + } + + .app-ports { + cursor: default; + min-width: 240px; + li { + padding-bottom: 7px; + white-space: nowrap; + + .port-wrapper { + display: inline-block; + min-width: 72px; + font-weight: bold; + font-size: 13px; + } + .arrow-wrapper { + text-align: center; + display: inline-block; + min-width: 28px; + font-size: 15px; + color: #999; + } + .open-button-wrapper { + display: inline-block; + } + .host-address-wrapper { + font-family: Monaco, monospace; + font-size: 12px; + cursor: text; + .text-select(); + } } } @@ -242,10 +349,6 @@ font-size: 12px; } - .disk-utilization { - margin-top: 2em; - } - .progress { font-weight: 600; height: 24px; diff --git a/meteor/client/stylesheets/intro.import.less b/meteor/client/stylesheets/intro.import.less new file mode 100644 index 0000000000..78fa0e6bb6 --- /dev/null +++ b/meteor/client/stylesheets/intro.import.less @@ -0,0 +1,15 @@ +.intro { + -webkit-app-region: drag; + height: 100%; + background: white; + + .content { + -webkit-app-region: no-drag; + margin-top: 220px; + text-align: center; + p { + .text-select(); + margin-top: 40px; + } + } +} diff --git a/meteor/client/stylesheets/radial-progress.import.less b/meteor/client/stylesheets/radial.import.less similarity index 58% rename from meteor/client/stylesheets/radial-progress.import.less rename to meteor/client/stylesheets/radial.import.less index f522a827d0..bb1b06c598 100644 --- a/meteor/client/stylesheets/radial-progress.import.less +++ b/meteor/client/stylesheets/radial.import.less @@ -1,42 +1,52 @@ +@-webkit-keyframes rotating { + from{ + -webkit-transform: rotate(0deg); + } + to{ + -webkit-transform: rotate(360deg); + } +} + .radial-progress { - @circle-size: 34px; - @circle-background: #d6dadc; - @circle-color: #3FD899; - @inset-size: 28px; - @inset-color: #fbfbfb; + + &.radial-spinner { + -webkit-animation: rotating 1.2s linear infinite; + } + + @circle-size: 96px; + @circle-background: transparent; + @inset-size: 92px; + @inset-color: white; @transition-length: 1s; - @shadow: 0px 1px 3px rgba(0,0,0,0.2); - @percentage-color: #3FD899; - @percentage-font-size: 11px; + // @percentage-color: #3FD899; + @percentage-font-size: 14px; @percentage-text-width: 57px; + margin: 0 auto; width: @circle-size; height: @circle-size; background-color: @circle-background; - border-radius: 50%; + border-radius: 100%; .circle { .mask, .fill, .shadow { width: @circle-size; height: @circle-size; position: absolute; - border-radius: 50%; - } - .shadow { - box-shadow: @shadow inset; + border-radius: 100%; } .mask, .fill { -webkit-backface-visibility: hidden; transition: -webkit-transform @transition-length; transition: -ms-transform @transition-length; transition: transform @transition-length; - border-radius: 50%; + border-radius: 100%; } - .mask { - clip: rect(0px, @circle-size, @circle-size, @circle-size/2); + .mask { + clip: rect(0px, @circle-size, @circle-size, @circle-size/2.0); .fill { - clip: rect(0px, @circle-size/2, @circle-size, 0px); - background-color: @circle-color; + clip: rect(0px, @circle-size/2.0, @circle-size, 0px); + background-color: @brand-action; } } } @@ -44,27 +54,35 @@ width: @inset-size; height: @inset-size; position: absolute; - margin-left: (@circle-size - @inset-size)/2; - margin-top: (@circle-size - @inset-size)/2; + margin-left: (@circle-size - @inset-size) / 2.0; + margin-top: (@circle-size - @inset-size) / 2.0; background-color: @inset-color; - border-radius: 50%; - box-shadow: @shadow; + border-radius: 100%; .percentage { width: @percentage-text-width; position: absolute; - top: (@inset-size - @percentage-font-size) / 2; - left: (@inset-size - @percentage-text-width) / 2; + top: (@inset-size - @percentage-font-size) / 2.0; + left: (@inset-size - @percentage-text-width) / 2.0; line-height: 1; text-align: center; - color: @percentage-color; - font-weight: 800; + // color: @percentage-color; + font-weight: 500; font-size: @percentage-font-size; } } + + &.radial-negative .circle .mask .fill { + background-color: @brand-negative; + } + + &.radial-positive .circle .mask .fill { + background-color: @brand-positive; + } + @i: 0; @increment: 180deg / 100; .loop (@i) when (@i <= 100) { diff --git a/meteor/client/stylesheets/setup.import.less b/meteor/client/stylesheets/setup.import.less deleted file mode 100644 index a520c3a160..0000000000 --- a/meteor/client/stylesheets/setup.import.less +++ /dev/null @@ -1,71 +0,0 @@ -.setup { - - -webkit-app-region: drag; - height: 100%; - background: #F8F8F8; - - .content { - margin-top: 30px; - } - - .steps { - margin-left: 180px; - margin-top: 60px; - .media-body { - margin-top: 5px; - font-size: 18px; - padding-left: 14px; - } - } - - h2 { - padding-top: 30px; - font-weight: 500; - font-size: 36px; - color: #3E3E3E; - } - - p { - margin-top: 20px; - font-size: 14px; - color: #232323; - font-weight: 500; - } - - .install_logo { - margin-top: 60px; - } - - .install-finish { - margin-top: 60px; - } - - .install-finish { - -webkit-app-region: no-drag; - margin-top: 80px; - p { - margin-top: 0; - font-size: 13px; - } - } - - .install-diagonistics { - -webkit-app-region: no-drag; - label { - text-align: left; - } - margin: 0 auto; - max-width: 300px; - margin-top: 50px; - } - - .install-continue { - -webkit-app-region: no-drag; - margin-top: 140px; - p { - margin-top: 0; - font-size: 13px; - } - } - -} diff --git a/meteor/client/stylesheets/theme.import.less b/meteor/client/stylesheets/theme.import.less index 1598310cb1..98f541d847 100755 --- a/meteor/client/stylesheets/theme.import.less +++ b/meteor/client/stylesheets/theme.import.less @@ -118,6 +118,24 @@ button { } } +.btn-action-inverse { + .btn-styles(@btn-action-color); + color: @btn-action-bg; + border-color: @btn-action-color; + &:hover, + &:focus { + border-color: darken(@btn-action-color, 10%); + background: darken(@btn-action-color, 10%); + color: @btn-action-bg; + } + &:active, + &.active { + border-color: darken(@btn-action-color, 10%); + background: darken(@btn-action-color, 10%); + color: @btn-action-bg; + } +} + .btn-positive { .btn-styles(@btn-positive-bg); color: @btn-positive-color; diff --git a/meteor/client/stylesheets/variables.import.less b/meteor/client/stylesheets/variables.import.less index a258b44eb4..3176195cc1 100755 --- a/meteor/client/stylesheets/variables.import.less +++ b/meteor/client/stylesheets/variables.import.less @@ -4,6 +4,7 @@ @font-size-base: 14px; @text-color: #343638; +@text-color-less-light: lighten(@text-color, 30%); @text-color-light: lighten(@text-color, 40%); @text-color-lighter: lighten(@text-color, 60%); @text-color-lightest: lighten(@text-color, 72%); diff --git a/meteor/client/views/application/layout.html b/meteor/client/views/application/layout.html index 1ceeafbe14..2bc12665fc 100755 --- a/meteor/client/views/application/layout.html +++ b/meteor/client/views/application/layout.html @@ -1,5 +1,3 @@ diff --git a/meteor/client/views/dashboard/apps/dashboard-apps-logs.html b/meteor/client/views/dashboard/apps/dashboard-apps-logs.html index 193d2db951..3d4f47645b 100644 --- a/meteor/client/views/dashboard/apps/dashboard-apps-logs.html +++ b/meteor/client/views/dashboard/apps/dashboard-apps-logs.html @@ -1,4 +1,4 @@ -