diff --git a/meteor/client/lib/virtualbox.js b/meteor/client/lib/virtualbox.js index 4368f5c72f..4d0cda78c6 100644 --- a/meteor/client/lib/virtualbox.js +++ b/meteor/client/lib/virtualbox.js @@ -4,9 +4,9 @@ var path = require('path'); VirtualBox = {}; -VirtualBox.REQUIRED_VERSION = '4.3.12'; -VirtualBox.INCLUDED_VERSION = '4.3.12'; -VirtualBox.INSTALLER_FILENAME = 'virtualbox-4.3.12.pkg'; +VirtualBox.REQUIRED_VERSION = '4.3.14'; +VirtualBox.INCLUDED_VERSION = '4.3.14'; +VirtualBox.INSTALLER_FILENAME = 'virtualbox-4.3.14.pkg'; // Info for the hostonly interface we add to the VM. VirtualBox.HOSTONLY_HOSTIP = '192.168.60.3'; @@ -25,6 +25,8 @@ VirtualBox.exec = function (command, callback) { 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) { + console.log(stdout); + console.log(stderr); if (error) { callback(error); return; diff --git a/meteor/server/lib/validations.js b/meteor/lib/validations.js similarity index 100% rename from meteor/server/lib/validations.js rename to meteor/lib/validations.js diff --git a/meteor/server/apps.js b/meteor/server/apps.js index d12a1e20a6..af9383afa3 100755 --- a/meteor/server/apps.js +++ b/meteor/server/apps.js @@ -1,55 +1,10 @@ -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 + '.kite @172.17.42.1 ', function() {}); - } else { - callback(null); - } +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.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) { +recoverApps = function (callback) { var apps = Apps.find({}).fetch(); _.each(apps, function (app) { // Update the app with the latest container info @@ -62,7 +17,7 @@ Apps.recover = function (callback) { console.log('restarting: ' + app.name); console.log(app.docker.Id); Fiber(function () { - Apps.restart(app, function (err) { + restartApp(app, function (err) { if (err) { console.error(err); } }); }).run(); @@ -74,8 +29,7 @@ Apps.recover = function (callback) { Meteor.methods({ recoverApps: function () { - this.unblock(); - return Meteor._wrapAsync(Apps.recover)(); + return Meteor._wrapAsync(recoverApps)(); }, configVar: function (appId, configVars) { this.unblock(); @@ -94,38 +48,64 @@ Meteor.methods({ if (!app) { throw new Meteor.Error(403, 'No app found with this ID'); } - Apps.remove({_id: app._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(); + }); + }); }, 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 appName = cleaned.name; - var appPath = path.join(KITE_PATH, appName); + var appObj = { + name: cleaned.name, + imageId: cleaned.imageId, + status: 'STARTING', + config: {} + }; + var appId = Apps.insert(appObj); + var appPath = path.join(KITE_PATH, appObj.name); if (!fs.existsSync(appPath)) { - console.log('Created Kite ' + appName + ' directory.'); + console.log('Created Kite ' + appObj.name + ' directory.'); fs.mkdirSync(appPath, function (err) { if (err) { throw err; } }); } - var appObj = { - name: appName, - imageId: cleaned.imageId, - status: 'STARTING', - config: {}, - path: appPath - }; - Apps.insert(appObj); + 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(); + }); } }, getAppLogs: function (appId) { this.unblock(); var app = Apps.findOne(appId); if (app) { - Apps.logs(app, function (err) { + getAppLogs(app, function (err) { if (err) { throw err; } }); } @@ -137,13 +117,12 @@ Meteor.methods({ Apps.update(app._id, {$set: { status: 'STARTING' }}); - Apps.restart(app, function (err) { + restartApp(app, function (err) { if (err) { console.error(err); } }); } }, resolveWatchers: function () { - this.unblock(); return Meteor._wrapAsync(resolveWatchers)(); } }); diff --git a/meteor/server/docker.js b/meteor/server/docker.js index 6978024245..d71b12688b 100755 --- a/meteor/server/docker.js +++ b/meteor/server/docker.js @@ -1,11 +1,16 @@ -Dockerode = Meteor.require('dockerode'); +Docker = Meteor.require('dockerode'); -var DOCKER_HOST='192.168.60.103'; -docker = new Dockerode({host: DOCKER_HOST, port: '2375'}); +var Convert = Meteor.require('ansi-to-html'); +var convert = new Convert(); -Docker = {}; +var DOCKER_HOST='192.168.59.103'; +docker = new Docker({host: '192.168.59.103', port: '2375'}); -Docker.removeContainer = function (containerId, callback) { +hasDockerfile = function (directory) { + return fs.existsSync(path.join(directory, 'Dockerfile')); +}; + +removeContainer = function (containerId, callback) { var container = docker.getContainer(containerId); container.kill(function (err) { if (err) { callback(err); return; } @@ -17,11 +22,28 @@ Docker.removeContainer = function (containerId, callback) { }); }; -Docker.removeContainerSync = function (containerId) { - return Meteor._wrapAsync(Docker.removeContainer)(containerId); +removeContainerSync = function (containerId) { + return Meteor._wrapAsync(removeContainer)(containerId); }; -Docker.getContainerData = function (containerId, callback) { +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) { var container = docker.getContainer(containerId); container.inspect(function (err, data) { if (err) { @@ -37,11 +59,11 @@ Docker.getContainerData = function (containerId, callback) { }); }; -Docker.getContainerDataSync = function (containerId) { - return Meteor._wrapAsync(Docker.getContainerData)(containerId); +getContainerDataSync = function (containerId) { + return Meteor._wrapAsync(getContainerData)(containerId); }; -Docker.runContainer = function (app, image, callback) { +runContainer = function (app, image, callback) { var envParam = []; _.each(_.keys(app.config), function (key) { var builtStr = key + '=' + app.config[key]; @@ -72,17 +94,17 @@ Docker.runContainer = function (app, image, callback) { if (err) { callback(err, null); return; } console.log('Started container: ' + container.id); // Use dig to refresh the DNS - exec('/usr/bin/dig dig ' + app.name + '.kite @172.17.42.1 ', function() {}); + exec('/usr/bin/dig dig ' + app.name + '.dev @172.17.42.1 ', function() {}); callback(null, container); }); }); }; -Docker.runContainerSync = function (app, image) { - return Meteor._wrapAsync(Docker.runContainer)(app, image); +runContainerSync = function (app, image) { + return Meteor._wrapAsync(runContainer)(app, image); }; -Docker.restartContainer = function (containerId, callback) { +restartContainer = function (containerId, callback) { var container = docker.getContainer(containerId); container.restart(function (err) { if (err) { @@ -95,8 +117,83 @@ Docker.restartContainer = function (containerId, callback) { }); }; -Docker.restartContainerSync = function (containerId) { - return Meteor._wrapAsync(Docker.restartContainer)(containerId); +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); }; var convertVolumeObjToArray = function (obj) { @@ -112,7 +209,7 @@ var convertVolumeObjToArray = function (obj) { return result; }; -Docker.getImageData = function (imageId, callback) { +getImageData = function (imageId, callback) { var image = docker.getImage(imageId.toLowerCase()); image.inspect(function (err, data) { if (err) { @@ -127,11 +224,11 @@ Docker.getImageData = function (imageId, callback) { }); }; -Docker.getImageDataSync = function (imageId) { - return Meteor._wrapAsync(Docker.getImageData)(imageId); +getImageDataSync = function (imageId) { + return Meteor._wrapAsync(getImageData)(imageId); }; -Docker.removeImage = function (imageId, callback) { +removeImage = function (imageId, callback) { var image = docker.getImage(imageId.toLowerCase()); image.remove({force: true}, function (err) { if (err) { callback(err); return; } @@ -140,14 +237,25 @@ Docker.removeImage = function (imageId, callback) { }); }; -Docker.removeImageSync = function (imageId) { - return Meteor._wrapAsync(Docker.removeImage)(imageId); +removeImageSync = function (imageId) { + return Meteor._wrapAsync(removeImage)(imageId); }; -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); - }); +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); }; var defaultContainerOptions = function () { @@ -196,7 +304,7 @@ resolveDefaultImages = function () { image.inspect(function (err) { if (err) { if (err.reason === 'no such image') { - docker.loadImage(path.join(Util.getBinDir(), 'base-images.tar.gz'), {}, function (err) { + docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) { if (err) { innerCallback(err); return; @@ -266,7 +374,7 @@ reloadDefaultContainers = function (callback) { async.until(function () { return ready; }, function (callback) { - docker.listContainers(function (err) { + docker.listContainers(function (err, containers) { if (!err) { ready = true; } @@ -283,7 +391,7 @@ reloadDefaultContainers = function (callback) { return; } console.log('Loading new Kitematic default images.'); - docker.loadImage(path.join(Util.getBinDir(), 'base-images.tar.gz'), {}, function (err) { + docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) { if (err) { callback(err); return; @@ -392,18 +500,142 @@ 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 { - Docker.removeContainerSync(app.name); + removeContainerSync(app.name); } catch (e) {} try { - var container = Docker.runContainerSync(app, image); - var containerData = Docker.getContainerDataSync(container.id); - // Set a delay for app to spin up + var container = runContainerSync(app, image); + var containerData = getContainerDataSync(container.id); Meteor.setTimeout(function () { Apps.update(app._id, {$set: { docker: containerData, @@ -418,23 +650,18 @@ 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/script/versions.sh b/script/versions.sh index e46149f22c..7cfc5c99c9 100644 --- a/script/versions.sh +++ b/script/versions.sh @@ -2,7 +2,7 @@ BASE_IMAGE_VERSION=0.0.2 BASE_IMAGE_VERSION_FILE=base-images-$BASE_IMAGE_VERSION.tar.gz BASE_IMAGE_FILE=base-images.tar.gz -VIRTUALBOX_FILE=virtualbox-4.3.12.pkg +VIRTUALBOX_FILE=virtualbox-4.3.14.pkg BOOT2DOCKER_CLI_VERSION=1.2.0 BOOT2DOCKER_CLI_VERSION_FILE=boot2docker-$BOOT2DOCKER_CLI_VERSION