diff --git a/meteor/.jshintrc b/meteor/.jshintrc index ab5b799ac1..dca33cf8e9 100755 --- a/meteor/.jshintrc +++ b/meteor/.jshintrc @@ -169,7 +169,11 @@ "Boot2Docker": true, "Installer": true, "VirtualBox": true, - + "ImageUtil": true, + "AppUtil": true, + + "getBinDir": true, + "getHomePath": true, "boot2dockerexec": true, "getBoot2DockerIp": true, "getBoot2DockerState": true, @@ -228,7 +232,8 @@ "KITE_PATH": true, "KITE_TAR_PATH": true, "KITE_IMAGES_PATH": true, - "COMMON_WEB_PORTS": true + "COMMON_WEB_PORTS": true, + "DOCKER_HOST": true } } diff --git a/meteor/.meteor/packages b/meteor/.meteor/packages index 0739b78bee..45a51bb89e 100755 --- a/meteor/.meteor/packages +++ b/meteor/.meteor/packages @@ -10,7 +10,6 @@ npm iron-router headers handlebar-helpers -collection2 collection-hooks moment underscore-string-latest diff --git a/meteor/client/lib/apps.js b/meteor/client/lib/apps.js new file mode 100644 index 0000000000..f8745c4306 --- /dev/null +++ b/meteor/client/lib/apps.js @@ -0,0 +1,119 @@ +AppUtil = {}; + +AppUtil.run = function (app) { + var image = Images.findOne({_id: app.imageId}); + // Delete old container if one already exists + Docker.removeContainer(app.name, function (err) { + if (err) { console.error(err); } + Docker.runContainer(app, image, function (err, container) { + if (err) { throw err; } + Docker.getContainerData(container.id, function (err, data) { + if (err) { console.error(err); } + // Set a delay for app to spin up + Meteor.setTimeout(function () { + Apps.update(app._id, {$set: { + docker: data, + status: 'READY' + }}); + }, 2500); + }); + }); + }); +}; + +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); } + Apps.update(app._id, {$set: { + status: 'READY', + docker: data + }}); + // Use dig to refresh the DNS + // exec('/usr/bin/dig dig ' + app.name + '.kite @172.17.42.1 ', function() {}); + }); + }); + } +}; + +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); + if (app.docker) { + Apps.remove({_id: appId}); + Docker.removeContainer(app.docker.Id, function (err) { + if (err) { console.error(err); } + var appPath = path.join(KITE_PATH, app.name); + Util.deleteFolder(appPath); + Docker.removeBindFolder(app.name, function () { + console.log('Deleted Kite ' + app.name + ' directory.'); + }); + }); + } +}; + +AppUtil.configVar = function (appId, configVars) { + Apps.update(appId, {$set: { + config: configVars, + status: 'STARTING' + }}); + var app = Apps.findOne({_id: appId}); + AppUtil.run(app); +}; + +AppUtil.logs = function (appId) { + var app = Apps.findOne(appId); + 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; } + Apps.update(app._id, { + $set: { + logs: [] + } + }); + var logs = []; + response.setEncoding('utf8'); + response.on('data', function (line) { + logs.push(convert.toHtml(line.slice(8))); + Apps.update(app._id, { + $set: { + logs: logs + } + }); + }); + response.on('end', function () {}); + }); + } +}; + +AppUtil.recover = function () { + var apps = Apps.find({}).fetch(); + _.each(apps, function (app) { + // Update the app with the latest container info + if (!app.docker) { + return; + } + var container = docker.getContainer(app.docker.Id); + container.inspect(function (err, data) { + if (app.status !== 'STARTING' && data && data.State && !data.State.Running) { + console.log('Restarting: ' + app.name); + console.log(app.docker.Id); + AppUtil.restartHelper(app, function (err) { + if (err) { console.error(err); } + }); + } + }); + }); +}; diff --git a/meteor/client/lib/boot2docker.js b/meteor/client/lib/boot2docker.js index 589b85b67d..7dfac20eb0 100644 --- a/meteor/client/lib/boot2docker.js +++ b/meteor/client/lib/boot2docker.js @@ -6,7 +6,7 @@ Boot2Docker = {}; Boot2Docker.REQUIRED_IP = '192.168.60.103'; Boot2Docker.exec = function (command, callback) { - exec(path.join(Util.getBinDir(), 'boot2docker') + ' ' + command, function(err, stdout, stderr) { + exec(path.join(getBinDir(), 'boot2docker') + ' ' + command, function(err, stdout, stderr) { callback(err, stdout, stderr); }); }; @@ -32,7 +32,7 @@ Boot2Docker.stop = function (callback) { }; Boot2Docker.erase = function (callback) { - var VMFileLocation = path.join(Util.getHomePath(), 'VirtualBox\\ VMs/boot2docker-vm'); + var VMFileLocation = path.join(getHomePath(), 'VirtualBox\\ VMs/boot2docker-vm'); exec('rm -rf ' + VMFileLocation, function (err) { callback(err); }); @@ -233,7 +233,7 @@ Boot2Docker.version = function (callback) { }; Boot2Docker.injectUtilities = function (callback) { - exec('/bin/cat ' + path.join(Util.getBinDir(), 'kite-binaries.tar.gz') + ' | ' + path.join(Util.getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) { + exec('/bin/cat ' + path.join(getBinDir(), 'kite-binaries.tar.gz') + ' | ' + path.join(getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) { callback(err); }); }; diff --git a/meteor/server/docker.js b/meteor/client/lib/docker.js old mode 100755 new mode 100644 similarity index 79% rename from meteor/server/docker.js rename to meteor/client/lib/docker.js index 6978024245..02d55c6954 --- a/meteor/server/docker.js +++ b/meteor/client/lib/docker.js @@ -1,6 +1,5 @@ -Dockerode = Meteor.require('dockerode'); +Dockerode = require('dockerode'); -var DOCKER_HOST='192.168.60.103'; docker = new Dockerode({host: DOCKER_HOST, port: '2375'}); Docker = {}; @@ -17,10 +16,6 @@ Docker.removeContainer = function (containerId, callback) { }); }; -Docker.removeContainerSync = function (containerId) { - return Meteor._wrapAsync(Docker.removeContainer)(containerId); -}; - Docker.getContainerData = function (containerId, callback) { var container = docker.getContainer(containerId); container.inspect(function (err, data) { @@ -37,10 +32,6 @@ Docker.getContainerData = function (containerId, callback) { }); }; -Docker.getContainerDataSync = function (containerId) { - return Meteor._wrapAsync(Docker.getContainerData)(containerId); -}; - Docker.runContainer = function (app, image, callback) { var envParam = []; _.each(_.keys(app.config), function (key) { @@ -78,10 +69,6 @@ Docker.runContainer = function (app, image, callback) { }); }; -Docker.runContainerSync = function (app, image) { - return Meteor._wrapAsync(Docker.runContainer)(app, image); -}; - Docker.restartContainer = function (containerId, callback) { var container = docker.getContainer(containerId); container.restart(function (err) { @@ -95,10 +82,6 @@ Docker.restartContainer = function (containerId, callback) { }); }; -Docker.restartContainerSync = function (containerId) { - return Meteor._wrapAsync(Docker.restartContainer)(containerId); -}; - var convertVolumeObjToArray = function (obj) { var result = []; if (obj !== null && typeof obj === 'object') { @@ -127,10 +110,6 @@ Docker.getImageData = function (imageId, callback) { }); }; -Docker.getImageDataSync = function (imageId) { - return Meteor._wrapAsync(Docker.getImageData)(imageId); -}; - Docker.removeImage = function (imageId, callback) { var image = docker.getImage(imageId.toLowerCase()); image.remove({force: true}, function (err) { @@ -140,12 +119,8 @@ Docker.removeImage = function (imageId, callback) { }); }; -Docker.removeImageSync = function (imageId) { - return Meteor._wrapAsync(Docker.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) { + exec(path.join(getBinDir(), 'boot2docker') + ' ssh "sudo rm -rf /var/lib/docker/binds/' + name + '"', function (err, stdout) { callback(err, stdout); }); }; @@ -196,7 +171,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; @@ -283,7 +258,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; @@ -391,50 +366,3 @@ killAndRemoveContainers = function (names, callback) { callback(err); }); }; - -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); - } catch (e) {} - try { - var container = Docker.runContainerSync(app, image); - var containerData = Docker.getContainerDataSync(container.id); - // Set a delay for app to spin up - Meteor.setTimeout(function () { - Apps.update(app._id, {$set: { - docker: containerData, - status: 'READY' - }}); - }, 2500); - } catch (e) { - console.error(e); - } - }, - getDockerHost: function () { - return DOCKER_HOST; - }, - reloadDefaultContainers: function () { - this.unblock(); - return Meteor._wrapAsync(reloadDefaultContainers)(); - }, - checkDefaultImages: function () { - this.unblock(); - return Meteor._wrapAsync(checkDefaultImages)(); - }, - resolveDefaultImages: function () { - this.unblock(); - return Meteor._wrapAsync(resolveDefaultImages)(); - }, - checkDefaultContainers: function () { - this.unblock(); - return Meteor._wrapAsync(checkDefaultContainers)(); - }, - resolveDefaultContainers: function () { - this.unblock(); - return Meteor._wrapAsync(resolveDefaultContainers)(); - } -}); diff --git a/meteor/client/lib/images.js b/meteor/client/lib/images.js new file mode 100644 index 0000000000..0c121d86ec --- /dev/null +++ b/meteor/client/lib/images.js @@ -0,0 +1,246 @@ +ImageUtil = {}; + +var createTarFile = function (image, callback) { + var TAR_PATH = path.join(KITE_TAR_PATH, image._id + '.tar'); + exec('tar czf ' + TAR_PATH + ' -C ' + image.path + ' .', function (err) { + if (err) { callback(err, null); return; } + console.log('Created tar file: ' + TAR_PATH); + callback(null, TAR_PATH); + }); +}; + +var getFromImage = function (dockerfile) { + var patternString = "(FROM)(.*)"; + var regex = new RegExp(patternString, "g"); + var fromInstruction = dockerfile.match(regex); + if (fromInstruction && fromInstruction.length > 0) { + return fromInstruction[0].replace('FROM', '').trim(); + } else { + return null; + } +}; + +var getImageJSON = function (directory) { + var KITE_JSON_PATH = path.join(directory, 'image.json'); + if (fs.existsSync(KITE_JSON_PATH)) { + var data = fs.readFileSync(KITE_JSON_PATH, 'utf8'); + return JSON.parse(data); + } else { + return null; + } +}; + +ImageUtil.getMetaData = function (directory) { + var kiteJSON = getImageJSON(directory); + if (kiteJSON) { + if (!kiteJSON.name) { + kiteJSON.name = _.last(directory.split(path.sep)); + } + } else { + kiteJSON = { + name: _.last(directory.split(path.sep)) + }; + } + return kiteJSON; +}; + +ImageUtil.saveFolder = function (directory, imageId, callback) { + var destinationPath = path.join(KITE_IMAGES_PATH, imageId); + if (!fs.existsSync(destinationPath)) { + fs.mkdirSync(destinationPath, function (err) { + if (err) { callback(err); return; } + }); + Util.copyFolder(directory, destinationPath); + console.log('Copied image folder for: ' + imageId); + callback(null); + } +}; + +ImageUtil.rebuild = function (image, callback) { + Util.deleteFolder(image.path); + var imageMetaData = ImageUtil.getMetaData(image.originPath); + if (imageMetaData.logo) { + Images.update(image._id, { + $set: { + logoPath: path.join(image.path, imageMetaData.logo) + } + }); + } else { + Images.update(image._id, { + $set: { + logoPath: null + } + }); + } + Images.update(image._id, { + $set: { + status: 'BUILDING', + meta: imageMetaData + } + }); + 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.rebuildImage = function (imageId) { + var image = Images.findOne(imageId); + if (!image) { + throw new Meteor.Error(403, "No image found with this ID."); + } + var apps = Apps.find({imageId: imageId}).fetch(); + if (apps.length > 0) { + _.each(apps, function (app) { + console.log('Updating app: ' + app.name); + if (app.docker) { + Docker.removeContainer(app.docker.Id, function (err) { + if (err) { console.error(err); } + }); + } + Apps.update(app._id, { + $set: { + 'docker.Id': null, + status: 'STARTING', + logs: [] + } + }); + }); + ImageUtil.rebuild(image, function (err) { + if (err) { console.error(err); } + }); + _.each(apps, function (app) { + app = Apps.findOne(app._id); + /*Meteor.call('runApp', app, function (err) { + if (err) { console.error(err); } + });*/ + }); + } else { + ImageUtil.rebuild(image, function (err) { + if (err) { console.error(err); } + }); + } +}, + +ImageUtil.pull = function (dockerfile, imageId, callback) { + var fromImage = getFromImage(dockerfile); + 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, { + $set: { + buildLogs: [] + } + }); + 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); + Images.update(imageId, { + $set: { + buildLogs: logs + } + }); + } catch (e) { + console.error(e); + } + }); + response.on('end', function () { + console.log('Finished pulling image: ' + fromImage); + callback(null); + }); + }); + } else { + callback(null); + } + }); +}; + +ImageUtil.build = function (image, callback) { + createTarFile(image, function (err, tarFilePath) { + if (err) { console.error(err); } + 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)); + Images.update(image._id, { + $set: { + buildLogs: logs + } + }); + } 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); + } + var imageData = null; + Docker.getImageData(image._id, function (err, data) { + if (err) { + Images.update(image._id, { + $set: { + status: 'ERROR' + } + }); + } else { + imageData = data; + Images.update(image._id, { + $set: { + docker: imageData, + status: 'READY' + } + }); + var oldImageId = null; + if (image.docker && image.docker.Id) { + oldImageId = image.docker.Id; + } + if (oldImageId && imageData && oldImageId !== imageData.Id) { + Docker.removeImage(oldImageId, function (err) { + if (err) { console.error(err); } + }); + } + } + callback(null); + }); + }); + }); + }); +}; diff --git a/meteor/client/lib/init/constants.js b/meteor/client/lib/init/constants.js new file mode 100644 index 0000000000..0aafe5ef0f --- /dev/null +++ b/meteor/client/lib/init/constants.js @@ -0,0 +1,33 @@ +var path = require('path'); + +getHomePath = function () { + return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; +}; + +getBinDir = function () { + if (process.env.NODE_ENV === 'development') { + return path.join(path.join(process.env.PWD, '..'), 'resources'); + } else { + if (Meteor.isClient) { + return path.join(process.cwd(), 'resources'); + } else { + return path.join(process.cwd(), '../../../resources'); + } + } +}; + +KITE_PATH = path.join(getHomePath(), 'Kitematic'); +KITE_TAR_PATH = path.join(KITE_PATH, '.tar'); +KITE_IMAGES_PATH = path.join(KITE_PATH, '.images'); + +DOCKER_HOST = '192.168.60.103'; + +COMMON_WEB_PORTS = [ + 80, + 8000, + 8080, + 3000, + 5000, + 2368, + 1337 +]; diff --git a/meteor/server/form-schemas.js b/meteor/client/lib/init/form-schemas.js old mode 100755 new mode 100644 similarity index 55% rename from meteor/server/form-schemas.js rename to meteor/client/lib/init/form-schemas.js index d3092abe04..ea165b9b54 --- a/meteor/server/form-schemas.js +++ b/meteor/client/lib/init/form-schemas.js @@ -27,18 +27,3 @@ FormSchema = { } }; - -// Auto-subscribe forms -_.each(_.keys(FormSchema), function (schemaName) { - console.log('Subscribed form schema: ' + schemaName); - var method = {}; - method[schemaName] = function (formInput) { - var result = formValidate(formInput, FormSchema[schemaName]); - if (result.errors) { - throw new Meteor.Error(400, 'Validation Failed.', result.errors); - } else { - return result.cleaned; - } - }; - Meteor.methods(method); -}); diff --git a/meteor/client/lib/init/requires.js b/meteor/client/lib/init/requires.js new file mode 100644 index 0000000000..78e07108dc --- /dev/null +++ b/meteor/client/lib/init/requires.js @@ -0,0 +1,10 @@ +https = require('https'); +tar = require('tar'); +zlib = require('zlib'); +fs = require('fs'); +path = require('path'); +exec = require('child_process').exec; +async = require('async'); +child_process = require('child_process'); +Convert = require('ansi-to-html'); +convert = new Convert(); diff --git a/meteor/client/lib/installer.js b/meteor/client/lib/installer.js index 459ed06f7b..a5bc41ec81 100644 --- a/meteor/client/lib/installer.js +++ b/meteor/client/lib/installer.js @@ -100,7 +100,7 @@ Installer.steps = [ }, pastMessage: 'Started the Boot2Docker VM', message: 'Starting the Boot2Docker VM', - futureMessage: 'Start the Kitematic VM', + futureMessage: 'Start the Kitematic VM' }, { @@ -117,14 +117,14 @@ Installer.steps = [ // Set up the default Kitematic images { run: function (callback) { - Meteor.call('reloadDefaultContainers', function (err) { - callback(err); + reloadDefaultContainers(function (err) { + if (err) { console.error(err); } }); }, pastMessage: 'Started the Boot2Docker VM', message: 'Setting up the default Kitematic images...', subMessage: '(This may take a few minutes)', - futureMessage: 'Set up the default Kitematic images', + futureMessage: 'Set up the default Kitematic images' } ]; diff --git a/meteor/client/lib/requires.js b/meteor/client/lib/requires.js deleted file mode 100644 index 2b261be406..0000000000 --- a/meteor/client/lib/requires.js +++ /dev/null @@ -1,2 +0,0 @@ -path = require('path'); -fs = require('fs'); diff --git a/meteor/client/lib/startup.js b/meteor/client/lib/startup.js index 512c2ffc32..51407e4460 100644 --- a/meteor/client/lib/startup.js +++ b/meteor/client/lib/startup.js @@ -1,3 +1,21 @@ Meteor.startup(function () { console.log('Kitematic started.'); + if (!fs.existsSync(KITE_PATH)) { + console.log('Created Kitematic directory.'); + fs.mkdirSync(KITE_PATH, function (err) { + if (err) { throw err; } + }); + } + if (!fs.existsSync(KITE_TAR_PATH)) { + console.log('Created Kitematic .tar directory.'); + fs.mkdirSync(KITE_TAR_PATH, function (err) { + if (err) { throw err; } + }); + } + if (!fs.existsSync(KITE_IMAGES_PATH)) { + console.log('Created Kitematic .images directory.'); + fs.mkdirSync(KITE_IMAGES_PATH, function (err) { + if (err) { throw err; } + }); + } }); diff --git a/meteor/client/lib/sync.js b/meteor/client/lib/sync.js index cd2468ef2d..242cabf89e 100644 --- a/meteor/client/lib/sync.js +++ b/meteor/client/lib/sync.js @@ -15,7 +15,7 @@ removeAppWatcher = function (id) { addAppWatcher = function (app) { removeAppWatcher(app._id); - var appPath = path.join(path.join(Util.getHomePath(), 'Kitematic'), app.name); + var appPath = path.join(path.join(getHomePath(), 'Kitematic'), app.name); var vmDir = path.join('/var/lib/docker/binds', app.name); var vmPath = 'ssh://docker@localhost:2022/' + vmDir; var watcher = chokidar.watch(appPath, {ignored: /.*\.DS_Store/}); @@ -30,7 +30,7 @@ addAppWatcher = function (app) { syncing = true; var errorPattern = /The\sfile\s(.*)\son\shost/g; var archiveErrorPattern = /Archive\s(.*)\son\shost\s.*\sshould\sbe\sDELETED/g; - var cmd = path.join(Util.getBinDir(), 'unison'); + var cmd = path.join(getBinDir(), 'unison'); var args = [ cmd, vmPath, @@ -46,7 +46,7 @@ addAppWatcher = function (app) { 'Name\ {*.tmp,*.unison,*.swp,*.pyc,.DS_Store}', '-auto', '-sshargs', - '-o\ UserKnownHostsFile=/dev/null\ -o\ StrictHostKeyChecking=no\ -o PreferredAuthentications=publickey\ -i\ ' + path.join(Util.getHomePath(), '.ssh/id_boot2docker') + '-o\ UserKnownHostsFile=/dev/null\ -o\ StrictHostKeyChecking=no\ -o PreferredAuthentications=publickey\ -i\ ' + path.join(getHomePath(), '.ssh/id_boot2docker') ]; if (!fs.existsSync(appPath)) { @@ -70,7 +70,7 @@ addAppWatcher = function (app) { if (err.indexOf('The archive file is missing on some hosts') !== -1) { var results = archiveErrorPattern.exec(err); var location = results[1].replace(' ', '\\ '); - var fullLocation = path.join(Util.getHomePath(), 'Library/Application\\ Support/Unison', location); + var fullLocation = path.join(getHomePath(), 'Library/Application\\ Support/Unison', location); var cmd = '/bin/rm -rf ' + fullLocation; exec(cmd, function () {}); } diff --git a/meteor/client/lib/utilities.js b/meteor/client/lib/utilities.js index 3315ad4040..5bc891436b 100755 --- a/meteor/client/lib/utilities.js +++ b/meteor/client/lib/utilities.js @@ -28,3 +28,137 @@ trackLink = function (trackLabel) { ga('send', 'event', 'link', 'click', trackLabel); } }; + +Util = {}; + +Util.deleteFolder = function (directory) { + if (fs.existsSync(directory)) { + fs.readdirSync(directory).forEach(function (file) { + var curDirectory = directory + '/' + file; + if (fs.lstatSync(curDirectory).isDirectory()) { + // Recurse + Util.deleteFolder(curDirectory); + } else { + // Delete File + try { + fs.unlinkSync(curDirectory); + } catch (e) { + console.error(e); + } + } + }); + fs.rmdirSync(directory); + } +}; + +Util.copyFolder = function (src, dest) { + var exists = fs.existsSync(src); + var stats = exists && fs.statSync(src); + var isDirectory = exists && stats.isDirectory(); + if (exists && isDirectory) { + try { + fs.mkdirSync(dest); + } catch (e) { + console.error(e); + } + fs.readdirSync(src).forEach(function (childItemName) { + Util.copyFolder(path.join(src, childItemName), path.join(dest, childItemName)); + }); + } else { + try { + fs.linkSync(src, dest); + } catch (e) { + console.error(e); + } + } +}; + +Util.copyVolumes = function (directory, appName) { + var KITE_VOLUMES_PATH = path.join(directory, 'volumes'); + if (fs.existsSync(KITE_VOLUMES_PATH)) { + var destinationPath = path.join(KITE_PATH, appName); + Util.copyFolder(KITE_VOLUMES_PATH, destinationPath); + console.log('Copied volumes for: ' + appName); + } +}; + +Util.hasDockerfile = function (directory) { + return fs.existsSync(path.join(directory, 'Dockerfile')); +}; + +/** + * Compares two software version numbers (e.g. "1.7.1" or "1.2b"). + * + * @param {string} v1 The first version to be compared. + * @param {string} v2 The second version to be compared. + * @param {object} [options] Optional flags that affect comparison behavior: + * + * @returns {number|NaN} + * + * + */ +Util.compareVersions = function (v1, v2, options) { + var lexicographical = options && options.lexicographical, + zeroExtend = options && options.zeroExtend, + v1parts = v1.split('.'), + v2parts = v2.split('.'); + + function isValidPart(x) { + return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x); + } + + if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { + return NaN; + } + + if (zeroExtend) { + while (v1parts.length < v2parts.length) { + v1parts.push('0'); + } + while (v2parts.length < v1parts.length) { + v2parts.push('0'); + } + } + + if (!lexicographical) { + v1parts = v1parts.map(Number); + v2parts = v2parts.map(Number); + } + + for (var i = 0; i < v1parts.length; ++i) { + if (v2parts.length === i) { + return 1; + } + + if (v1parts[i] === v2parts[i]) { + continue; + } + else if (v1parts[i] > v2parts[i]) { + return 1; + } + else { + return -1; + } + } + + if (v1parts.length !== v2parts.length) { + return -1; + } + + return 0; +}; diff --git a/meteor/server/lib/validations.js b/meteor/client/lib/validations.js old mode 100755 new mode 100644 similarity index 100% rename from meteor/server/lib/validations.js rename to meteor/client/lib/validations.js diff --git a/meteor/client/lib/virtualbox.js b/meteor/client/lib/virtualbox.js index 4368f5c72f..577c610797 100644 --- a/meteor/client/lib/virtualbox.js +++ b/meteor/client/lib/virtualbox.js @@ -24,7 +24,7 @@ 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) { + exec('open -W ' + path.join(getBinDir(), this.INSTALLER_FILENAME), function (error, stdout, stderr) { if (error) { callback(error); return; @@ -73,7 +73,7 @@ VirtualBox.hostOnlyIfs = function (callback) { currentIf = value; hostOnlyIfs[value] = {}; } - hostOnlyIfs[currentIf][key] = value; + hostOnlyIfs[currentIf][key] = value; }); callback(null, hostOnlyIfs); }); @@ -134,8 +134,8 @@ VirtualBox.addCustomHostAdapter = function (vm, callback) { VirtualBox.setupRouting = function (vm, callback) { // Get the host only adapter or create it if it doesn't exist this.addCustomHostAdapter(vm, function (err, ifname) { - var installFile = path.join(Util.getBinDir(), 'install'); - var cocoaSudo = path.join(Util.getBinDir(), 'cocoasudo'); + var installFile = path.join(getBinDir(), 'install'); + var cocoaSudo = path.join(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) { diff --git a/meteor/client/main.js b/meteor/client/main.js index 429201d295..8b28f32921 100755 --- a/meteor/client/main.js +++ b/meteor/client/main.js @@ -63,11 +63,6 @@ Handlebars.registerHelper('timeSince', function (date) { return moment(date).fromNow(); }); -Meteor.call('getDockerHost', function (err, host) { - if (err) { throw err; } - Session.set('dockerHost', host); -}); - fixBoot2DockerVM = function (callback) { Boot2Docker.check(function (err) { if (err) { @@ -87,10 +82,10 @@ fixBoot2DockerVM = function (callback) { }; fixDefaultImages = function (callback) { - Meteor.call('checkDefaultImages', function (err) { + checkDefaultImages(function (err) { if (err) { Session.set('available', false); - Meteor.call('resolveDefaultImages', function (err) { + resolveDefaultImages(function (err) { if (err) { callback(); } else { @@ -106,10 +101,10 @@ fixDefaultImages = function (callback) { }; fixDefaultContainers = function (callback) { - Meteor.call('checkDefaultContainers', function (err) { + checkDefaultContainers(function (err) { if (err) { Session.set('available', false); - Meteor.call('resolveDefaultContainers', function (err) { + resolveDefaultContainers(function (err) { if (err) { callback(err); } else { @@ -151,7 +146,7 @@ Meteor.setInterval(function () { if (!Session.get('boot2dockerOff')) { fixBoot2DockerVM(function (err) { if (err) { console.log(err); return; } - Meteor.call('recoverApps'); + AppUtil.recover(); fixDefaultImages(function (err) { if (err) { console.log(err); return; } fixDefaultContainers(function (err) { @@ -162,4 +157,3 @@ Meteor.setInterval(function () { } } }, 5000); - diff --git a/meteor/client/views/dashboard/apps/dashboard-apps-settings.js b/meteor/client/views/dashboard/apps/dashboard-apps-settings.js index 4b65f986d0..ca119c5d9a 100755 --- a/meteor/client/views/dashboard/apps/dashboard-apps-settings.js +++ b/meteor/client/views/dashboard/apps/dashboard-apps-settings.js @@ -19,9 +19,8 @@ Template.dashboard_apps_settings.events({ var envKey = $button.data('key'); var configVars = getConfigVars($form); delete configVars[envKey]; - Meteor.call('configVar', appId, configVars, function () { - $button.removeAttr('disabled'); - }); + AppUtil.configVar(appId, configVars); + $button.removeAttr('disabled'); }, 'submit .form-env-vars': function (e) { var $form = $(e.currentTarget); @@ -31,10 +30,9 @@ Template.dashboard_apps_settings.events({ var newVal = $form.find('input[name="env-var-value"]').val().trim(); if (newKey && newVal) { configVars[newKey] = newVal; - Meteor.call('configVar', appId, configVars, function () { - $form.find('input[name="env-var-key"]').val(''); - $form.find('input[name="env-var-value"]').val(''); - }); + AppUtil.configVar(appId, configVars); + $form.find('input[name="env-var-key"]').val(''); + $form.find('input[name="env-var-value"]').val(''); } e.preventDefault(); e.stopPropagation(); @@ -43,9 +41,7 @@ Template.dashboard_apps_settings.events({ 'click .btn-delete-app': function () { var result = confirm("Are you sure you want to delete this app?"); if (result === true) { - Meteor.call('deleteApp', this._id, function (err) { - if (err) { throw err; } - }); + AppUtil.remove(this._id); Router.go('dashboard_apps'); } } diff --git a/meteor/client/views/dashboard/apps/dashboard-single-app.js b/meteor/client/views/dashboard/apps/dashboard-single-app.js index 5d32c4818f..964c082d3c 100755 --- a/meteor/client/views/dashboard/apps/dashboard-single-app.js +++ b/meteor/client/views/dashboard/apps/dashboard-single-app.js @@ -21,8 +21,8 @@ Template.dashboard_single_app.events({ }, 'click .btn-terminal': function () { var app = this; - var cmd = path.join(Util.getBinDir(), 'boot2docker') + ' ssh -t "sudo docker-enter ' + app.docker.Id + '"'; - var terminalCmd = path.join(Util.getBinDir(), 'terminal') + ' ' + cmd; + var cmd = path.join(getBinDir(), 'boot2docker') + ' ssh -t "sudo docker-enter ' + app.docker.Id + '"'; + var terminalCmd = path.join(getBinDir(), 'terminal') + ' ' + cmd; var exec = require('child_process').exec; console.log(terminalCmd); exec(terminalCmd, function (err, stdout) { @@ -33,9 +33,7 @@ Template.dashboard_single_app.events({ }); }, 'click .btn-restart': function () { - Meteor.call('restartApp', this._id, function (err) { - if (err) { throw err; } - }); + AppUtil.restart(this._id); }, 'click .btn-folder': function () { var exec = require('child_process').exec; @@ -44,8 +42,6 @@ Template.dashboard_single_app.events({ }); }, 'click .btn-logs': function () { - Meteor.call('getAppLogs', this._id, function (err) { - if (err) { throw err; } - }); + AppUtil.logs(this._id); } }); diff --git a/meteor/client/views/dashboard/components/modal-create-app.js b/meteor/client/views/dashboard/components/modal-create-app.js index 1458d0566d..19f5d75f69 100755 --- a/meteor/client/views/dashboard/components/modal-create-app.js +++ b/meteor/client/views/dashboard/components/modal-create-app.js @@ -8,24 +8,53 @@ Template.modal_create_app.events({ 'submit #form-create-app': function (e) { var $form = $(e.currentTarget); var formData = $form.serializeObject(); - Meteor.call('formCreateApp', formData, function (errors, cleaned) { - if (errors) { - clearFormErrors($form); - showFormErrors($form, errors.details); - } else { - clearFormErrors($form); - Meteor.call('createApp', cleaned, function (err) { + var validationResult = formValidate(formData, FormSchema.formCreateApp); + if (validationResult.errors) { + clearFormErrors($form); + showFormErrors($form, validationResult.errors.details); + } else { + clearFormErrors($form); + var cleaned = validationResult.cleaned; + var appName = cleaned.name; + var appPath = path.join(KITE_PATH, appName); + if (!fs.existsSync(appPath)) { + console.log('Created Kite ' + appName + ' directory.'); + fs.mkdirSync(appPath, function (err) { if (err) { throw err; } }); - $('#modal-create-app').bind('hidden.bs.modal', function () { - $('#slug-create-app-name').html(''); - resetForm($form); - $('#image-picker').find('.fa-check-square-o').hide(); - $('#image-picker').find('.fa-square-o').show(); - Router.go('dashboard_apps'); - }).modal('hide'); } - }); + var appObj = { + name: appName, + imageId: cleaned.imageId, + status: 'STARTING', + config: {}, + path: appPath, + logs: [], + createdAt: new Date() + }; + var appId = Apps.insert(appObj); + Apps.update(appId, { + $set: { + 'config.APP_ID': appId + } + }); + var app = Apps.findOne(appId); + var image = Images.findOne(app.imageId); + Util.copyVolumes(image.path, app.name); + Docker.removeBindFolder(app.name, function (err) { + if (err) { console.error(err); } + AppUtil.run(app, function (err) { + if (err) { console.error(err); } + }); + }); + $('#modal-create-app').bind('hidden.bs.modal', function () { + $('#slug-create-app-name').html(''); + resetForm($form); + $('#image-picker').find('.fa-check-square-o').hide(); + $('#image-picker').find('.fa-square-o').show(); + Router.go('dashboard_apps'); + }).modal('hide'); + } e.preventDefault(); e.stopPropagation(); return false; diff --git a/meteor/client/views/dashboard/components/modal-create-image.js b/meteor/client/views/dashboard/components/modal-create-image.js index 25e16aac51..8b212222a1 100755 --- a/meteor/client/views/dashboard/components/modal-create-image.js +++ b/meteor/client/views/dashboard/components/modal-create-image.js @@ -20,26 +20,55 @@ Template.modal_create_image.events({ $('#picked-directory-error').html(''); if (pickedDirectory) { $('#picked-directory').html('' + pickedDirectory + ''); - Meteor.call('validateDirectory', pickedDirectory, function (err) { - if (err) { - $('#picked-directory-error').html(err.reason); - $('#btn-create-image').attr('disabled', 'disabled'); - } else { - $('#btn-create-image').removeAttr('disabled'); - } - }); + if (!Util.hasDockerfile(pickedDirectory)) { + $('#picked-directory-error').html('Only directories with Dockerfiles are supported now.'); + $('#btn-create-image').attr('disabled', 'disabled'); + } else { + $('#btn-create-image').removeAttr('disabled'); + } } else { $('#picked-directory').html(''); $('#btn-create-image').attr('disabled', 'disabled'); } }, 'click #btn-create-image': function () { - var pickedDirectory = $('#directory-picker').val(); + var directory = $('#directory-picker').val(); $('#directory-picker').val(''); $('#picked-directory-error').html(''); $('#picked-directory').html(''); $('#btn-create-image').attr('disabled', 'disabled'); $('#modal-create-image').modal('hide'); - Meteor.call('createImage', pickedDirectory); + var imageObj = { + status: 'BUILDING', + originPath: directory, + buildLogs: [], + createdAt: new Date() + }; + var imageMetaData = ImageUtil.getMetaData(directory); + imageObj.meta = imageMetaData; + var imageId = Images.insert(imageObj); + var imagePath = path.join(KITE_IMAGES_PATH, imageId); + Images.update(imageId, { + $set: { + path: imagePath + } + }); + if (imageObj.meta.logo) { + Images.update(imageId, { + $set: { + logoPath: path.join(imagePath, imageObj.meta.logo) + } + }); + } + var image = Images.findOne(imageId); + ImageUtil.saveFolder(image.originPath, imageId, function (err) { + if (err) { console.error(err); } + ImageUtil.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), imageId, function (err) { + if (err) { throw err; } + ImageUtil.build(image, function (err) { + if (err) { console.error(err); } + }); + }); + }); } }); diff --git a/meteor/client/views/dashboard/images/dashboard-images-settings.js b/meteor/client/views/dashboard/images/dashboard-images-settings.js index cfb1f8aa2d..764be3cf3e 100755 --- a/meteor/client/views/dashboard/images/dashboard-images-settings.js +++ b/meteor/client/views/dashboard/images/dashboard-images-settings.js @@ -2,15 +2,27 @@ Template.dashboard_images_settings.events({ 'click .btn-delete-image': function () { var result = confirm("Are you sure you want to delete this image?"); if (result === true) { - Meteor.call('deleteImage', this._id, function (err) { - if (err) { - $('#error-delete-image').html('' + err.reason + ''); - $('#error-delete-image').fadeIn(); - } else { - removeAppWatcher(this._id); - Router.go('dashboard_images'); + var imageId = this._id; + var image = Images.findOne(imageId); + var app = Apps.findOne({imageId: imageId}); + if (!app) { + Images.remove({_id: image._id}); + if (image.docker) { + Docker.removeImage(image.docker.Id, function (err) { + if (err) { console.error(err); } + }); } - }); + try { + Util.deleteFolder(image.path); + } catch (e) { + console.error(e); + } + removeAppWatcher(imageId); + Router.go('dashboard_images'); + } else { + $('#error-delete-image').html('This image is currently being used by ' + app.name + '.'); + $('#error-delete-image').fadeIn(); + } } }, 'click #btn-pick-directory': function () { @@ -22,15 +34,15 @@ Template.dashboard_images_settings.events({ var pickedDirectory = $picker.val(); $('#picked-directory-error').html(''); if (pickedDirectory) { - Meteor.call('validateDirectory', pickedDirectory, function (err) { - if (err) { - $('#picked-directory-error').html(err.reason); - } else { - Meteor.call('changeDirectory', imageId, pickedDirectory, function (err) { - if (err) { throw err; } - }); - } - }); + if (!Util.hasDockerfile(pickedDirectory)) { + $('#picked-directory-error').html('Only directories with Dockerfiles are supported now.'); + } else { + Images.update(imageId, { + $set: { + originPath: pickedDirectory + } + }); + } } } }); diff --git a/meteor/client/views/dashboard/images/dashboard-single-image.js b/meteor/client/views/dashboard/images/dashboard-single-image.js index b6c44f1b27..aa0723790e 100755 --- a/meteor/client/views/dashboard/images/dashboard-single-image.js +++ b/meteor/client/views/dashboard/images/dashboard-single-image.js @@ -18,8 +18,8 @@ Template.dashboard_single_image.events({ }, 'click .btn-rebuild': function () { $('.btn-icon').tooltip('hide'); - Meteor.call('rebuildImage', this._id, function (err) { - if (err) { throw err; } + ImageUtil.rebuildImage(this._id, function (err) { + if (err) { console.error(err); } }); } }); diff --git a/meteor/client/views/dashboard/layouts/dashboard-apps-layout.js b/meteor/client/views/dashboard/layouts/dashboard-apps-layout.js index ae4b40de04..9a32743715 100644 --- a/meteor/client/views/dashboard/layouts/dashboard-apps-layout.js +++ b/meteor/client/views/dashboard/layouts/dashboard-apps-layout.js @@ -21,9 +21,7 @@ Template.dashboard_apps_layout.events({ $('.header .icons a').tooltip('hide'); }, 'click .btn-logs': function () { - Meteor.call('getAppLogs', this._id, function (err) { - if (err) { throw err; } - }); + AppUtil.logs(this._id); }, 'click .btn-terminal': function () { var buildCmd = function (dockerId, termApp) { @@ -42,9 +40,7 @@ Template.dashboard_apps_layout.events({ }); }, 'click .btn-restart': function () { - Meteor.call('restartApp', this._id, function (err) { - if (err) { throw err; } - }); + AppUtil.restart(this._id); }, 'click .btn-folder': function () { var exec = require('child_process').exec; diff --git a/meteor/client/views/dashboard/layouts/dashboard-images-layout.js b/meteor/client/views/dashboard/layouts/dashboard-images-layout.js index 78e84305c3..f4051a5332 100755 --- a/meteor/client/views/dashboard/layouts/dashboard-images-layout.js +++ b/meteor/client/views/dashboard/layouts/dashboard-images-layout.js @@ -18,8 +18,8 @@ Template.dashboard_images_layout.events({ }, 'click .btn-rebuild': function () { $('.header .icons a').tooltip('hide'); - Meteor.call('rebuildImage', this._id, function (err) { - if (err) { throw err; } + ImageUtil.rebuildImage(this._id, function (err) { + if (err) { console.error(err); } }); } }); diff --git a/meteor/collections/apps.js b/meteor/collections/apps.js index 85cf5c5266..df5836415e 100755 --- a/meteor/collections/apps.js +++ b/meteor/collections/apps.js @@ -1,57 +1,14 @@ Apps = new Meteor.Collection('apps'); -schemaApps = new SimpleSchema({ - imageId: { - type: Meteor.ObjectID, - label: "ID of the image used by the app", - max: 200 +Apps.allow({ + 'update': function () { + return true; }, - docker: { - type: Object, - label: "Docker container data", - blackbox: true, - optional: true + 'insert': function () { + return true; }, - status: { - type: String, - allowedValues: ['STARTING', 'READY', 'ERROR'], - label: "App current status", - max: 200 - }, - config: { - type: Object, - label: "App environment variables", - blackbox: true - }, - name: { - type: String, - label: "App name", - max: 200 - }, - logs: { - type: [String], - label: "Logs", - defaultValue: [] - }, - path: { - type: String, - label: "Path to the app directory", - optional: true - }, - createdAt: { - type: Date, - autoValue: function() { - var now = new Date(); - if (this.isInsert) { - return now; - } else if (this.isUpsert) { - return {$setOnInsert: now}; - } else { - this.unset(); - } - }, - denyUpdate: true, - label: "Time of app created" + 'remove': function () { + return true; } }); @@ -114,43 +71,3 @@ Apps.helpers({ } } }); - -Apps.attachSchema(schemaApps); - -Apps.after.insert(function (userId, app) { - // Give app an unique environment variable - var appId = this._id; - Apps.update(appId, { - $set: { - 'config.APP_ID': appId - } - }); - var image = Images.findOne(app.imageId); - Util.copyVolumes(image.path, app.name); - app = Apps.findOne(appId); - Docker.removeBindFolder(app.name, function (err) { - if (err) { - console.error(err); - } - Fiber(function () { - Meteor.call('runApp', app, function (err) { - if (err) { throw err; } - }); - }).run(); - }); -}); - -Apps.after.remove(function (userId, app) { - if (app.docker) { - try { - Docker.removeContainerSync(app.docker.Id); - } catch (e) { - console.error(e); - } - } - var appPath = path.join(KITE_PATH, app.name); - Util.deleteFolder(appPath); - Docker.removeBindFolder(app.name, function () { - console.log('Deleted Kite ' + app.name + ' directory.'); - }); -}); diff --git a/meteor/collections/images.js b/meteor/collections/images.js index 5a4de5b4f8..1c94dfe578 100755 --- a/meteor/collections/images.js +++ b/meteor/collections/images.js @@ -1,61 +1,5 @@ Images = new Meteor.Collection('images'); -schemaImages = new SimpleSchema({ - path: { - type: String, - label: "Path to the image directory", - optional: true - }, - originPath: { - type: String, - label: "Path to the folder where image is built from", - optional: true - }, - logoPath: { - type: String, - label: "Path to the image logo", - optional: true - }, - meta: { - type: Object, - label: "Meta data for the image", - blackbox: true, - optional: true - }, - docker: { - type: Object, - label: "Docker image data", - blackbox: true, - optional: true - }, - status: { - type: String, - allowedValues: ['BUILDING', 'READY', 'ERROR'], - label: "Image build current status", - max: 200 - }, - buildLogs: { - type: [String], - label: "Build logs", - defaultValue: [] - }, - createdAt: { - type: Date, - autoValue: function() { - var now = new Date(); - if (this.isInsert) { - return now; - } else if (this.isUpsert) { - return {$setOnInsert: now}; - } else { - this.unset(); - } - }, - denyUpdate: true, - label: "Time of image created" - } -}); - Images.helpers({ downloadStatus: function () { if (this.buildLogs.length > 0) { @@ -89,45 +33,3 @@ Images.allow({ return true; } }); - -Images.attachSchema(schemaImages); - -Images.after.insert(function (userId, image) { - var imageId = this._id; - var imagePath = path.join(KITE_IMAGES_PATH, imageId); - Images.update(imageId, { - $set: { - path: imagePath - } - }); - if (image.meta.logo) { - Images.update(imageId, { - $set: { - logoPath: path.join(imagePath, image.meta.logo) - } - }); - } - image = Images.findOne(imageId); - Images.saveFolderSync(image.originPath, imageId); - Images.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), imageId, function (err) { - if (err) { throw err; } - Images.build(image, function (err) { - if (err) { console.error(err); } - }); - }); -}); - -Images.after.remove(function (userId, image) { - if (image.docker) { - try { - Docker.removeImageSync(image.docker.Id); - } catch (e) { - console.error(e); - } - } - try { - Util.deleteFolder(image.path); - } catch (e) { - console.error(e); - } -}); diff --git a/meteor/collections/installs.js b/meteor/collections/installs.js index 314b538050..7b1311731e 100644 --- a/meteor/collections/installs.js +++ b/meteor/collections/installs.js @@ -1,28 +1,5 @@ Installs = new Meteor.Collection('installs'); -schemaInstalls = new SimpleSchema({ - createdAt: { - type: Date, - autoValue: function() { - var now = new Date(); - if (this.isInsert) { - return now; - } else if (this.isUpsert) { - return {$setOnInsert: now}; - } else { - this.unset(); - } - }, - denyUpdate: true, - label: 'Time of install created' - }, - version: { - type: String, - label: 'Installed version', - optional: true - } -}); - Installs.allow({ 'update': function () { return true; @@ -34,5 +11,3 @@ Installs.allow({ return true; } }); - -Installs.attachSchema(schemaInstalls); \ No newline at end of file diff --git a/meteor/lib/constants.js b/meteor/lib/constants.js deleted file mode 100644 index b53ada417d..0000000000 --- a/meteor/lib/constants.js +++ /dev/null @@ -1,9 +0,0 @@ -COMMON_WEB_PORTS = [ - 80, - 8000, - 8080, - 3000, - 5000, - 2368, - 1337 -] diff --git a/meteor/lib/utilities.js b/meteor/lib/utilities.js deleted file mode 100644 index 363ab245e7..0000000000 --- a/meteor/lib/utilities.js +++ /dev/null @@ -1,146 +0,0 @@ -Util = {}; - -Util.getHomePath = function () { - return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; -}; - -Util.getBinDir = function () { - if (process.env.NODE_ENV === 'development') { - return path.join(path.join(process.env.PWD, '..'), 'resources'); - } else { - if (Meteor.isClient) { - return path.join(process.cwd(), 'resources'); - } else { - return path.join(process.cwd(), '../../../resources'); - } - } -}; - -Util.deleteFolder = function (directory) { - if (fs.existsSync(directory)) { - fs.readdirSync(directory).forEach(function (file) { - var curDirectory = directory + '/' + file; - if (fs.lstatSync(curDirectory).isDirectory()) { - // Recurse - Util.deleteFolder(curDirectory); - } else { - // Delete File - try { - fs.unlinkSync(curDirectory); - } catch (e) { - console.error(e); - } - } - }); - fs.rmdirSync(directory); - } -}; - -Util.copyFolder = function (src, dest) { - var exists = fs.existsSync(src); - var stats = exists && fs.statSync(src); - var isDirectory = exists && stats.isDirectory(); - if (exists && isDirectory) { - try { - fs.mkdirSync(dest); - } catch (e) { - console.error(e); - } - fs.readdirSync(src).forEach(function (childItemName) { - Util.copyFolder(path.join(src, childItemName), path.join(dest, childItemName)); - }); - } else { - try { - fs.linkSync(src, dest); - } catch (e) { - console.error(e); - } - } -}; - -Util.copyVolumes = function (directory, appName) { - var KITE_VOLUMES_PATH = path.join(directory, 'volumes'); - if (fs.existsSync(KITE_VOLUMES_PATH)) { - var destinationPath = path.join(KITE_PATH, appName); - Util.copyFolder(KITE_VOLUMES_PATH, destinationPath); - console.log('Copied volumes for: ' + appName); - } -}; - -Util.hasDockerfile = function (directory) { - return fs.existsSync(path.join(directory, 'Dockerfile')); -}; - -/** - * Compares two software version numbers (e.g. "1.7.1" or "1.2b"). - * - * @param {string} v1 The first version to be compared. - * @param {string} v2 The second version to be compared. - * @param {object} [options] Optional flags that affect comparison behavior: - * - * @returns {number|NaN} - * - * - */ -Util.compareVersions = function (v1, v2, options) { - var lexicographical = options && options.lexicographical, - zeroExtend = options && options.zeroExtend, - v1parts = v1.split('.'), - v2parts = v2.split('.'); - - function isValidPart(x) { - return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x); - } - - if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { - return NaN; - } - - if (zeroExtend) { - while (v1parts.length < v2parts.length) v1parts.push('0'); - while (v2parts.length < v1parts.length) v2parts.push('0'); - } - - if (!lexicographical) { - v1parts = v1parts.map(Number); - v2parts = v2parts.map(Number); - } - - for (var i = 0; i < v1parts.length; ++i) { - if (v2parts.length == i) { - return 1; - } - - if (v1parts[i] == v2parts[i]) { - continue; - } - else if (v1parts[i] > v2parts[i]) { - return 1; - } - else { - return -1; - } - } - - if (v1parts.length != v2parts.length) { - return -1; - } - - return 0; -}; - diff --git a/meteor/server/apps.js b/meteor/server/apps.js deleted file mode 100755 index d12a1e20a6..0000000000 --- a/meteor/server/apps.js +++ /dev/null @@ -1,149 +0,0 @@ -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); - } -}; - -Apps.logs = function (app) { - if (app.docker && app.docker.Id) { - var container = docker.getContainer(app.docker.Id); - container.logs({follow: false, stdout: true, stderr: true, timestamps: true, tail: 300}, function (err, response) { - if (err) { throw err; } - Fiber(function () { - Apps.update(app._id, { - $set: { - logs: [] - } - }); - }).run(); - var logs = []; - response.setEncoding('utf8'); - response.on('data', function (line) { - logs.push(convert.toHtml(line.slice(8))); - Fiber(function () { - Apps.update(app._id, { - $set: { - logs: logs - } - }); - }).run(); - }); - response.on('end', function () {}); - }); - } -}; - -Apps.recover = function (callback) { - var apps = Apps.find({}).fetch(); - _.each(apps, function (app) { - // Update the app with the latest container info - if (!app.docker) { - return; - } - var container = docker.getContainer(app.docker.Id); - container.inspect(function (err, data) { - if (app.status !== 'STARTING' && data && data.State && !data.State.Running) { - console.log('restarting: ' + app.name); - console.log(app.docker.Id); - Fiber(function () { - Apps.restart(app, function (err) { - if (err) { console.error(err); } - }); - }).run(); - } - }); - }); - callback(); -}; - -Meteor.methods({ - recoverApps: function () { - this.unblock(); - return Meteor._wrapAsync(Apps.recover)(); - }, - configVar: function (appId, configVars) { - this.unblock(); - Apps.update(appId, {$set: { - config: configVars, - status: 'STARTING' - }}); - var app = Apps.findOne({_id: appId}); - Meteor.call('runApp', app, function (err) { - if (err) { console.error(err); } - }); - }, - deleteApp: function (appId) { - this.unblock(); - var app = Apps.findOne(appId); - if (!app) { - throw new Meteor.Error(403, 'No app found with this ID'); - } - Apps.remove({_id: app._id}); - }, - createApp: function (formData) { - this.unblock(); - var validationResult = formValidate(formData, FormSchema.formCreateApp); - if (validationResult.errors) { - throw new Meteor.Error(400, 'Validation Failed.', validationResult.errors); - } else { - var cleaned = validationResult.cleaned; - var appName = cleaned.name; - var appPath = path.join(KITE_PATH, appName); - if (!fs.existsSync(appPath)) { - console.log('Created Kite ' + appName + ' 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); - } - }, - getAppLogs: function (appId) { - this.unblock(); - var app = Apps.findOne(appId); - if (app) { - Apps.logs(app, function (err) { - if (err) { throw err; } - }); - } - }, - restartApp: function (appId) { - this.unblock(); - var app = Apps.findOne(appId); - if (app && app.docker) { - Apps.update(app._id, {$set: { - status: 'STARTING' - }}); - Apps.restart(app, function (err) { - if (err) { console.error(err); } - }); - } - }, - resolveWatchers: function () { - this.unblock(); - return Meteor._wrapAsync(resolveWatchers)(); - } -}); diff --git a/meteor/server/constants.js b/meteor/server/constants.js deleted file mode 100644 index cb35d90451..0000000000 --- a/meteor/server/constants.js +++ /dev/null @@ -1,24 +0,0 @@ -KITE_PATH = path.join(Util.getHomePath(), 'Kitematic'); -KITE_TAR_PATH = path.join(KITE_PATH, '.tar'); -KITE_IMAGES_PATH = path.join(KITE_PATH, '.images'); - -if (!fs.existsSync(KITE_PATH)) { - console.log('Created Kitematic directory.'); - fs.mkdirSync(KITE_PATH, function (err) { - if (err) { throw err; } - }); -} - -if (!fs.existsSync(KITE_TAR_PATH)) { - console.log('Created Kitematic .tar directory.'); - fs.mkdirSync(KITE_TAR_PATH, function (err) { - if (err) { throw err; } - }); -} - -if (!fs.existsSync(KITE_IMAGES_PATH)) { - console.log('Created Kitematic .images directory.'); - fs.mkdirSync(KITE_IMAGES_PATH, function (err) { - if (err) { throw err; } - }); -} diff --git a/meteor/server/images.js b/meteor/server/images.js deleted file mode 100755 index 6ad43fe7d0..0000000000 --- a/meteor/server/images.js +++ /dev/null @@ -1,306 +0,0 @@ -var createTarFile = function (image, callback) { - var TAR_PATH = path.join(KITE_TAR_PATH, image._id + '.tar'); - exec('tar czf ' + TAR_PATH + ' -C ' + image.path + ' .', function (err) { - if (err) { callback(err, null); return; } - console.log('Created tar file: ' + TAR_PATH); - callback(null, TAR_PATH); - }); -}; - -var createTarFileSync = function (image) { - return Meteor._wrapAsync(createTarFile)(image); -}; - -var getFromImage = function (dockerfile) { - var patternString = "(FROM)(.*)"; - var regex = new RegExp(patternString, "g"); - var fromInstruction = dockerfile.match(regex); - if (fromInstruction && fromInstruction.length > 0) { - return fromInstruction[0].replace('FROM', '').trim(); - } else { - return null; - } -}; - -var getImageJSON = function (directory) { - var KITE_JSON_PATH = path.join(directory, 'image.json'); - if (fs.existsSync(KITE_JSON_PATH)) { - var data = fs.readFileSync(KITE_JSON_PATH, 'utf8'); - return JSON.parse(data); - } else { - return null; - } -}; - -var getImageMetaData = function (directory) { - var kiteJSON = getImageJSON(directory); - if (kiteJSON) { - if (!kiteJSON.name) { - kiteJSON.name = _.last(directory.split(path.sep)); - } - } else { - kiteJSON = { - name: _.last(directory.split(path.sep)) - }; - } - return kiteJSON; -}; - -Images.saveFolder = function (directory, imageId, callback) { - var destinationPath = path.join(KITE_IMAGES_PATH, imageId); - if (!fs.existsSync(destinationPath)) { - fs.mkdirSync(destinationPath, function (err) { - if (err) { callback(err); return; } - }); - Util.copyFolder(directory, destinationPath); - console.log('Copied image folder for: ' + imageId); - callback(null); - } -}; - -Images.saveFolderSync = function (directory, imageId) { - return Meteor._wrapAsync(Images.saveFolder)(directory, imageId); -}; - -Images.rebuild = function (image, callback) { - Util.deleteFolder(image.path); - var imageMetaData = getImageMetaData(image.originPath); - if (imageMetaData.logo) { - Images.update(image._id, { - $set: { - logoPath: path.join(image.path, imageMetaData.logo) - } - }); - } else { - Images.update(image._id, { - $set: { - logoPath: null - } - }); - } - Images.update(image._id, { - $set: { - status: 'BUILDING', - meta: imageMetaData - } - }); - image = Images.findOne(image._id); - Images.saveFolderSync(image.originPath, image._id); - Images.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) { - if (err) { callback(err, null); return; } - Images.build(image, function (err) { - if (err) { console.error(err); } - callback(null, null); - }); - }); -}; - -Images.rebuildSync = function (image) { - return Meteor._wrapAsync(Images.rebuild)(image); -}; - -Images.pull = function (dockerfile, imageId, callback) { - var fromImage = getFromImage(dockerfile); - console.log('From image: ' + fromImage); - var installedImage = null; - try { - installedImage = Docker.getImageDataSync(fromImage); - } catch (e) { - console.error(e); - } - if (fromImage && !installedImage) { - Fiber(function () { - Images.update(imageId, { - $set: { - buildLogs: [] - } - }); - }).run(); - var logs = []; - docker.pull(fromImage, function (err, response) { - if (err) { callback(err); return; } - response.setEncoding('utf8'); - response.on('data', function (data) { - try { - var logData = JSON.parse(data); - var logDisplay = ''; - if (logData.id) { - logDisplay += logData.id + ' | '; - } - logDisplay += logData.status; - if (logData.progressDetail && logData.progressDetail.current && logData.progressDetail.total) { - logDisplay += ' - ' + Math.round(logData.progressDetail.current / logData.progressDetail.total * 100) + '%'; - } - logs.push(logDisplay); - Fiber(function () { - Images.update(imageId, { - $set: { - buildLogs: logs - } - }); - }).run(); - } catch (e) { - console.error(e); - } - }); - response.on('end', function () { - console.log('Finished pulling image: ' + fromImage); - callback(null); - }); - }); - } else { - callback(null); - } -}; - -Images.build = function (image, callback) { - Fiber(function () { - var tarFilePath = createTarFileSync(image); - Images.update(image._id, { - $set: { - buildLogs: [] - } - }); - docker.buildImage(tarFilePath, {t: image._id.toLowerCase()}, function (err, response) { - if (err) { callback(err); } - console.log('Building Docker image...'); - var logs = []; - response.setEncoding('utf8'); - response.on('data', function (data) { - try { - var line = JSON.parse(data).stream; - logs.push(convert.toHtml(line)); - Fiber(function () { - Images.update(image._id, { - $set: { - buildLogs: logs - } - }); - }).run(); - } catch (e) { - console.error(e); - } - }); - response.on('end', function () { - console.log('Finished building Docker image.'); - try { - fs.unlinkSync(tarFilePath); - console.log('Cleaned up tar file.'); - } catch (e) { - console.error(e); - } - Fiber(function () { - var imageData = null; - try { - imageData = Docker.getImageDataSync(image._id); - Images.update(image._id, { - $set: { - docker: imageData, - status: 'READY' - } - }); - } catch (e) { - console.log(e); - Images.update(image._id, { - $set: { - status: 'ERROR' - } - }); - } - var oldImageId = null; - if (image.docker && image.docker.Id) { - oldImageId = image.docker.Id; - } - if (oldImageId && imageData && oldImageId !== imageData.Id) { - try { - Docker.removeImageSync(oldImageId); - } catch (e) { - console.error(e); - } - } - }).run(); - callback(null); - }); - }); - }).run(); -}; - -Meteor.methods({ - createImage: function (directory) { - this.unblock(); - var imageObj = { - status: 'BUILDING', - originPath: directory - }; - var imageMetaData = getImageMetaData(directory); - imageObj.meta = imageMetaData; - Images.insert(imageObj); - }, - rebuildImage: function (imageId) { - this.unblock(); - var image = Images.findOne(imageId); - if (!image) { - throw new Meteor.Error(403, "No image found with this ID."); - } - var apps = Apps.find({imageId: imageId}).fetch(); - if (apps.length > 0) { - _.each(apps, function (app) { - console.log('Updating app: ' + app.name); - if (app.docker) { - try { - Docker.removeContainerSync(app.docker.Id); - } catch (e) { - console.error(e); - } - } - Apps.update(app._id, { - $set: { - 'docker.Id': null, - status: 'STARTING', - logs: [] - } - }); - }); - Images.rebuildSync(image); - _.each(apps, function (app) { - app = Apps.findOne(app._id); - Meteor.call('runApp', app, function (err) { - if (err) { console.error(err); } - }); - }); - } else { - Images.rebuildSync(image); - } - }, - changeDirectory: function (imageId, directory) { - this.unblock(); - var image = Images.findOne(imageId); - if (!image) { - throw new Meteor.Error(403, "No image found with this ID."); - } - Images.update(imageId, { - $set: { - originPath: directory - } - }); - }, - validateDirectory: function (directory) { - this.unblock(); - if (!Util.hasDockerfile(directory)) { - throw new Meteor.Error(400, "Only directories with Dockerfiles are supported now."); - } - }, - deleteImage: function (imageId) { - this.unblock(); - var image = Images.findOne(imageId); - if (!image) { - throw new Meteor.Error(403, "No image found with this ID."); - } - var app = Apps.findOne({imageId: imageId}); - if (!app) { - Images.remove({_id: image._id}); - } else { - throw new Meteor.Error(400, 'This image is currently being used by ' + app.name + "."); - } - } -}); diff --git a/meteor/server/lib/requires.js b/meteor/server/lib/requires.js deleted file mode 100755 index 6174cd0202..0000000000 --- a/meteor/server/lib/requires.js +++ /dev/null @@ -1,11 +0,0 @@ -https = Meteor.require('https'); -tar = Meteor.require('tar'); -zlib = Meteor.require('zlib'); -fs = Meteor.require('fs'); -path = Meteor.require('path'); -exec = Meteor.require('child_process').exec; -async = Meteor.require('async'); -Fiber = Meteor.require('fibers'); -child_process = Meteor.require('child_process'); -Convert = Meteor.require('ansi-to-html'); -convert = new Convert(); diff --git a/meteor/smart.json b/meteor/smart.json index 6959af2f2d..6680b5eb87 100755 --- a/meteor/smart.json +++ b/meteor/smart.json @@ -5,7 +5,6 @@ "npm": {}, "headers": {}, "handlebar-helpers": {}, - "collection2": {}, "collection-hooks": {}, "moment": {}, "underscore-string-latest": {}, diff --git a/meteor/smart.lock b/meteor/smart.lock index 20d2835feb..bceed7a0ad 100755 --- a/meteor/smart.lock +++ b/meteor/smart.lock @@ -7,7 +7,6 @@ "npm": {}, "headers": {}, "handlebar-helpers": {}, - "collection2": {}, "collection-hooks": {}, "moment": {}, "underscore-string-latest": {}, @@ -42,11 +41,6 @@ "tag": "v0.1.1", "commit": "0b407ab65e7c1ebd53d71aef0de2e2c1d21a597c" }, - "collection2": { - "git": "https://github.com/aldeed/meteor-collection2.git", - "tag": "v0.4.6", - "commit": "80554182486be0d8e74f7ed02194a5649d712e27" - }, "collection-hooks": { "git": "https://github.com/matb33/meteor-collection-hooks.git", "tag": "v0.7.2", @@ -92,11 +86,6 @@ "tag": "v0.0.8", "commit": "90f2fbcc5b4bc17fa4d4535f47813e31d86033b4" }, - "simple-schema": { - "git": "https://github.com/aldeed/meteor-simple-schema.git", - "tag": "v0.7.0", - "commit": "77d267aec4ba8a70f677e5d9ef9fb91fb0e3f0f6" - }, "blaze-layout": { "git": "https://github.com/EventedMind/blaze-layout.git", "tag": "v0.2.5", diff --git a/package.json b/package.json index 62c09055d8..cca8152c33 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "version": "0.1.0", "window": { "show": false, - "toolbar": false, - "frame": false, + "toolbar": true, + "frame": true, "width": 800, "height": 600, "resizable": false @@ -19,6 +19,9 @@ "chokidar": "git+https://github.com/usekite/chokidar.git", "exec": "^0.1.2", "moment": "2.8.1", - "open": "0.0.5" + "open": "0.0.5", + "dockerode": "2.0.3", + "tar": "0.1.20", + "ansi-to-html": "0.2.0" } }