Merge branch 'backend-refactor' of github.com:kitematic/kitematic into installer_fix

Conflicts:
	meteor/client/lib/boot2docker.js
	meteor/client/lib/utilities.js
	meteor/client/lib/virtualbox.js
	meteor/client/main.js
	meteor/client/views/dashboard/setup/setup-install.js
	meteor/server/docker.js
This commit is contained in:
Jeff Morgan 2014-09-01 04:00:06 -07:00
commit b66bbc2c0d
24 changed files with 630 additions and 656 deletions

View File

@ -1,5 +1,10 @@
# [Kitematic](https://kitematic.com) # [Kitematic](https://kitematic.com)
**Note:** If the installer gets stuck at any step for more than 1 minute, there is probably an error. Please help us troubleshoot by running it from the command line, and submit the logs to [contact@kitematic.com](mailto:contact@kitematic.com).
1. `cd <dir with Kitematic.app>`
2. Run `./Kitematic.app/Contents/MacOS/node-webkit`
Kitematic is still in Beta. Any effort in helping us find issues and improving the experience is greatly appreciated! Kitematic is still in Beta. Any effort in helping us find issues and improving the experience is greatly appreciated!
[Fixes for Known Issues](http://kitematic.com/docs/known-issue-fixes) [Fixes for Known Issues](http://kitematic.com/docs/known-issue-fixes)
@ -73,20 +78,28 @@ For transparency into our release cycle and in striving to maintain backward com
## Creators ## Creators
Team E-mail: [contact@kitematic.com](mailto:contact@kitematic.com)
**Sean Li** **Sean Li**
- <https://twitter.com/lisean106> - <https://twitter.com/lisean106>
- <https://github.com/Elesant> - <https://github.com/Elesant>
- Email: [sean@kitematic.com](mailto:sean@kitematic.com)
- [LinkedIn](https://www.linkedin.com/in/lishang)
**Jeffrey Morgan** **Jeffrey Morgan**
- <https://twitter.com/jmorgan> - <https://twitter.com/jmorgan>
- <https://github.com/jeffdm> - <https://github.com/jeffdm>
- Email: [jeff@kitematic.com](mailto:jeff@kitematic.com)
- [LinkedIn](https://www.linkedin.com/in/jeffdmorgan)
**Michael Chiang** **Michael Chiang**
- <https://twitter.com/mchiang0610> - <https://twitter.com/mchiang0610>
- <https://github.com/mk101> - <https://github.com/mk101>
- Email: [mike@kitematic.com](mailto:mike@kitematic.com)
- [LinkedIn](https://www.linkedin.com/in/mchiang0610)
## Copyright and License ## Copyright and License

View File

@ -128,7 +128,7 @@
// Packages // Packages
"Fiber": true, "Fiber": true,
"moment": true, "moment": true,
"Docker": true, "Dockerode": true,
"byline": true, "byline": true,
"fs": true, "fs": true,
"zlib": true, "zlib": true,
@ -142,6 +142,9 @@
"chokidar": true, "chokidar": true,
"docker": true, "docker": true,
"async": true, "async": true,
"child_process": true,
"convert": true,
"Convert": true,
// Collections // Collections
"SimpleSchema": false, "SimpleSchema": false,
@ -161,8 +164,13 @@
"SetupController": true, "SetupController": true,
// Server and Client // Server and Client
"Docker": true,
"Util": true,
"Boot2Docker": true,
"Installer": true,
"VirtualBox": true,
"boot2dockerexec": true, "boot2dockerexec": true,
"getBinDir": true,
"getBoot2DockerIp": true, "getBoot2DockerIp": true,
"getBoot2DockerState": true, "getBoot2DockerState": true,
"getBoot2DockerDiskUsage": true, "getBoot2DockerDiskUsage": true,
@ -170,7 +178,6 @@
"getBoot2DockerInfo": true, "getBoot2DockerInfo": true,
"boot2DockerVMExists": true, "boot2DockerVMExists": true,
"eraseBoot2DockerVMFiles": true, "eraseBoot2DockerVMFiles": true,
"getHomePath": true,
"initBoot2Docker": true, "initBoot2Docker": true,
"isVirtualBoxInstalled": true, "isVirtualBoxInstalled": true,
"upgradeBoot2Docker": true, "upgradeBoot2Docker": true,
@ -191,46 +198,17 @@
"fixInterval": true, "fixInterval": true,
"stopFixInterval": true, "stopFixInterval": true,
"runSetup": true, "runSetup": true,
"removeBindFolder": true,
"removeAppWatcher": true, "removeAppWatcher": true,
"addAppWatcher": true, "addAppWatcher": true,
"resolveWatchers": true, "resolveWatchers": true,
"recoverApps": true,
"restartApp": true,
"deleteApp": true,
"deleteFolder": true,
"loadKiteVolumes": true,
"getAppLogs": true,
"hasDockerfile": true,
"runContainer": true,
"runContainerSync": true,
"restartContainer": true,
"restartContainerSync": true,
"createTarFile": true,
"createTarFileSync": true,
"getImageData": true,
"getImageDataSync": true,
"removeImage": true,
"removeImageSync": true,
"deleteImage": true,
"checkDefaultImages": true, "checkDefaultImages": true,
"resolveDefaultImages": true, "resolveDefaultImages": true,
"checkDefaultContainers": true, "checkDefaultContainers": true,
"resolveDefaultContainers": true, "resolveDefaultContainers": true,
"killAndRemoveContainers": true, "killAndRemoveContainers": true,
"deleteImageSync": true,
"upContainers": true, "upContainers": true,
"reloadDefaultContainers": true, "reloadDefaultContainers": true,
"removeImages": true, "removeImages": true,
"pullImageFromDockerfile": true,
"buildImage": true,
"getImageMetaData": true,
"getImageJSON": true,
"rebuildImage": true,
"saveImageFolderSync": true,
"rebuildImageSync": true,
"saveImageFolder": true,
"copyFolder": true,
// Forms // Forms
"showFormErrors": true, "showFormErrors": true,
@ -239,11 +217,6 @@
"FormSchema": true, "FormSchema": true,
"showFormSuccess": true, "showFormSuccess": true,
"resetForm": true, "resetForm": true,
"removeContainer": true,
"removeContainerSync": true,
"deleteAppSync": true,
"getContainerData": true,
"getContainerDataSync": true,
// Testing // Testing
"require": false, "require": false,

View File

@ -6,7 +6,7 @@ Boot2Docker = {};
Boot2Docker.REQUIRED_IP = '192.168.60.103'; Boot2Docker.REQUIRED_IP = '192.168.60.103';
Boot2Docker.exec = function (command, callback) { Boot2Docker.exec = function (command, callback) {
exec(path.join(getBinDir(), 'boot2docker') + ' ' + command, function(err, stdout, stderr) { exec(path.join(Util.getBinDir(), 'boot2docker') + ' ' + command, function(err, stdout, stderr) {
callback(err, stdout, stderr); callback(err, stdout, stderr);
}); });
}; };
@ -77,22 +77,14 @@ Boot2Docker.start = function (callback) {
return; return;
} }
self.exec('up -v', function (err, stdout) { self.exec('up -v', function (err, stdout) {
console.log('here0');
console.log('here1');
// Sometimes boot2docker returns an error code even though it's working / waiting, so treat that as // Sometimes boot2docker returns an error code even though it's working / waiting, so treat that as
// Success as well // Success as well
if (!err || (err.indexOf('Waiting for VM to be started') !== -1 || err.indexOf('..........') !== -1)) { if (!err || (err.indexOf('Waiting for VM to be started') !== -1 || err.indexOf('..........') !== -1)) {
Boot2Docker.setIp('eth2', Boot2Docker.REQUIRED_IP, function(err) { self.correct(function (err) {
console.log('here1');
if (err) { callback(err); return; }
VirtualBox.removeDHCP(function (err) {
console.log('here2');
self.injectUtilities(function (err) { self.injectUtilities(function (err) {
console.log('here3');
callback(err); callback(err);
}); });
}); })
});
} else { } else {
callback(err); callback(err);
} }
@ -100,6 +92,15 @@ Boot2Docker.start = function (callback) {
}); });
}; };
Boot2Docker.correct = function (callback) {
Boot2Docker.setIp('eth2', Boot2Docker.REQUIRED_IP, function(err) {
if (err) { callback(err); return; }
VirtualBox.removeDHCP(function (err) {
callback(err);
});
});
};
Boot2Docker.state = function (callback) { Boot2Docker.state = function (callback) {
this.exec('info', function (err, stdout) { this.exec('info', function (err, stdout) {
if (err) { if (err) {
@ -228,7 +229,7 @@ Boot2Docker.version = function (callback) {
} }
var match = stdout.match(/Client version: v(\d\.\d\.\d)/); var match = stdout.match(/Client version: v(\d\.\d\.\d)/);
if (!match || match.length < 2) { if (!match || match.length < 2) {
callback('Could not parse the boot2docker cli version.') callback('Could not parse the boot2docker cli version.');
} else { } else {
callback(null, match[1]); callback(null, match[1]);
} }
@ -236,14 +237,14 @@ Boot2Docker.version = function (callback) {
}; };
Boot2Docker.injectUtilities = function (callback) { Boot2Docker.injectUtilities = function (callback) {
exec('/bin/cat ' + path.join(getBinDir(), 'kite-binaries.tar.gz') + ' | ' + path.join(getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) { exec('/bin/cat ' + path.join(Util.getBinDir(), 'kite-binaries.tar.gz') + ' | ' + path.join(Util.getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) {
callback(err); callback(err);
}); });
}; };
Boot2Docker.check = function (callback) { Boot2Docker.check = function (callback) {
var self = this; var self = this;
self.exists(function (err) { self.exists(function (err, exists) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
@ -252,7 +253,9 @@ Boot2Docker.check = function (callback) {
if (state !== 'running') { if (state !== 'running') {
callback('boot2docker not running'); callback('boot2docker not running');
} else { } else {
callback(); self.correct(function (err) {
callback(err);
});
} }
}); });
} }
@ -264,8 +267,8 @@ Boot2Docker.resolve = function (callback) {
self.exists(function (err, exists) { self.exists(function (err, exists) {
// If somehow the boot2docker VM doesn't exist anymor then re-create it. // If somehow the boot2docker VM doesn't exist anymor then re-create it.
if (!exists) { if (!exists) {
initBoot2Docker(function () { self.init(function () {
startBoot2Docker(function (err) { self.start(function (err) {
callback(err); callback(err);
}); });
}); });
@ -273,7 +276,7 @@ Boot2Docker.resolve = function (callback) {
// If it exists but it's not running.. restart it. // If it exists but it's not running.. restart it.
self.state(function (err, state) { self.state(function (err, state) {
if (state !== 'running') { if (state !== 'running') {
startBoot2Docker(function (err) { self.start(function (err) {
callback(err); callback(err);
}); });
} else { } else {

View File

@ -0,0 +1,2 @@
path = require('path');
fs = require('fs');

View File

@ -53,7 +53,6 @@ Router.map(function () {
console.log('No installs detected, running installer again.'); console.log('No installs detected, running installer again.');
this.redirect('/setup/intro'); this.redirect('/setup/intro');
} else { } else {
startFixInterval();
this.redirect('/apps'); this.redirect('/apps');
} }
} }

View File

@ -15,7 +15,7 @@ removeAppWatcher = function (id) {
addAppWatcher = function (app) { addAppWatcher = function (app) {
removeAppWatcher(app._id); removeAppWatcher(app._id);
var appPath = path.join(path.join(getHomePath(), 'Kitematic'), app.name); var appPath = path.join(path.join(Util.getHomePath(), 'Kitematic'), app.name);
var vmDir = path.join('/var/lib/docker/binds', app.name); var vmDir = path.join('/var/lib/docker/binds', app.name);
var vmPath = 'ssh://docker@localhost:2022/' + vmDir; var vmPath = 'ssh://docker@localhost:2022/' + vmDir;
var watcher = chokidar.watch(appPath, {ignored: /.*\.DS_Store/}); var watcher = chokidar.watch(appPath, {ignored: /.*\.DS_Store/});
@ -30,7 +30,7 @@ addAppWatcher = function (app) {
syncing = true; syncing = true;
var errorPattern = /The\sfile\s(.*)\son\shost/g; var errorPattern = /The\sfile\s(.*)\son\shost/g;
var archiveErrorPattern = /Archive\s(.*)\son\shost\s.*\sshould\sbe\sDELETED/g; var archiveErrorPattern = /Archive\s(.*)\son\shost\s.*\sshould\sbe\sDELETED/g;
var cmd = path.join(getBinDir(), 'unison'); var cmd = path.join(Util.getBinDir(), 'unison');
var args = [ var args = [
cmd, cmd,
vmPath, vmPath,
@ -46,7 +46,7 @@ addAppWatcher = function (app) {
'Name\ {*.tmp,*.unison,*.swp,*.pyc,.DS_Store}', 'Name\ {*.tmp,*.unison,*.swp,*.pyc,.DS_Store}',
'-auto', '-auto',
'-sshargs', '-sshargs',
'-o\ UserKnownHostsFile=/dev/null\ -o\ StrictHostKeyChecking=no\ -o PreferredAuthentications=publickey\ -i\ ' + path.join(getHomePath(), '.ssh/id_boot2docker') '-o\ UserKnownHostsFile=/dev/null\ -o\ StrictHostKeyChecking=no\ -o PreferredAuthentications=publickey\ -i\ ' + path.join(Util.getHomePath(), '.ssh/id_boot2docker')
]; ];
if (!fs.existsSync(appPath)) { if (!fs.existsSync(appPath)) {
@ -70,7 +70,7 @@ addAppWatcher = function (app) {
if (err.indexOf('The archive file is missing on some hosts') !== -1) { if (err.indexOf('The archive file is missing on some hosts') !== -1) {
var results = archiveErrorPattern.exec(err); var results = archiveErrorPattern.exec(err);
var location = results[1].replace(' ', '\\ '); var location = results[1].replace(' ', '\\ ');
var fullLocation = path.join(getHomePath(), 'Library/Application\\ Support/Unison', location); var fullLocation = path.join(Util.getHomePath(), 'Library/Application\\ Support/Unison', location);
var cmd = '/bin/rm -rf ' + fullLocation; var cmd = '/bin/rm -rf ' + fullLocation;
exec(cmd, function () {}); exec(cmd, function () {});
} }

View File

@ -1,92 +1,3 @@
var path = require('path');
Utilities = {};
/**
* Compares two software version numbers (e.g. "1.7.1" or "1.2b").
*
* @param {string} v1 The first version to be compared.
* @param {string} v2 The second version to be compared.
* @param {object} [options] Optional flags that affect comparison behavior:
* <ul>
* <li>
* <tt>lexicographical: true</tt> compares each part of the version strings lexicographically instead of
* naturally; this allows suffixes such as "b" or "dev" but will cause "1.10" to be considered smaller than
* "1.2".
* </li>
* <li>
* <tt>zeroExtend: true</tt> changes the result if one version string has less parts than the other. In
* this case the shorter string will be padded with "zero" parts instead of being considered smaller.
* </li>
* </ul>
* @returns {number|NaN}
* <ul>
* <li>0 if the versions are equal</li>
* <li>a negative integer iff v1 < v2</li>
* <li>a positive integer iff v1 > v2</li>
* <li>NaN if either version string is in the wrong format</li>
* </ul>
*
*/
Utilities.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;
};
getBinDir = function () {
if (process.env.NODE_ENV === 'development') {
return path.join(path.join(process.env.PWD, '..'), 'resources');
} else {
return path.join(process.cwd(), 'resources');
}
};
getHomePath = function () {
return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
};
showFormErrors = function ($form, errors) { showFormErrors = function ($form, errors) {
for (var name in errors) { for (var name in errors) {
if (errors.hasOwnProperty(name)) { if (errors.hasOwnProperty(name)) {

View File

@ -24,14 +24,14 @@ VirtualBox.exec = function (command, callback) {
VirtualBox.install = function (callback) { VirtualBox.install = function (callback) {
// -W waits for the process to close before finishing. // -W waits for the process to close before finishing.
exec('open -W ' + path.join(getBinDir(), this.INSTALLER_FILENAME), function (error, stdout, stderr) { exec('open -W ' + path.join(Util.getBinDir(), this.INSTALLER_FILENAME), function (error, stdout, stderr) {
if (error) { if (error) {
callback(error); callback(error);
return; return;
} }
callback(null); callback(null);
}); });
} };
VirtualBox.version = function (callback) { VirtualBox.version = function (callback) {
if (!this.installed()) { if (!this.installed()) {
@ -63,7 +63,9 @@ VirtualBox.hostOnlyIfs = function (callback) {
var hostOnlyIfs = {}; var hostOnlyIfs = {};
var currentIf = null; var currentIf = null;
_.each(lines, function (line) { _.each(lines, function (line) {
if (!line.length) return; if (!line.length) {
return;
}
var pieces = line.split(':'); var pieces = line.split(':');
var key = pieces[0].trim(); var key = pieces[0].trim();
var value = pieces[1] ? pieces[1].trim() : null; var value = pieces[1] ? pieces[1].trim() : null;
@ -139,8 +141,8 @@ VirtualBox.addCustomHostAdapter = function (vm, callback) {
VirtualBox.setupRouting = function (vm, callback) { VirtualBox.setupRouting = function (vm, callback) {
// Get the host only adapter or create it if it doesn't exist // Get the host only adapter or create it if it doesn't exist
this.addCustomHostAdapter(vm, function (err, ifname) { this.addCustomHostAdapter(vm, function (err, ifname) {
var installFile = path.join(getBinDir(), 'install'); var installFile = path.join(Util.getBinDir(), 'install');
var cocoaSudo = path.join(getBinDir(), 'cocoasudo'); var cocoaSudo = path.join(Util.getBinDir(), 'cocoasudo');
var execCommand = cocoaSudo + ' --prompt="Kitematic needs your password to allow routing *.dev requests to containers." ' + installFile; var execCommand = cocoaSudo + ' --prompt="Kitematic needs your password to allow routing *.dev requests to containers." ' + installFile;
exec(execCommand, {env: {IFNAME: ifname, GATEWAY: Boot2Docker.REQUIRED_IP}}, function (error, stdout, stderr) { exec(execCommand, {env: {IFNAME: ifname, GATEWAY: Boot2Docker.REQUIRED_IP}}, function (error, stdout, stderr) {
if (error) { if (error) {
@ -152,17 +154,12 @@ VirtualBox.setupRouting = function (vm, callback) {
}); });
}; };
VirtualBox.removeDHCP = function (callback) { VirtualBox.removeDHCP = function (callback) {
var self = this; var self = this;
self.hostOnlyAdapter(function (err, ifname) { self.hostOnlyAdapter(function (err, ifname) {
if (err) { callback(err); return; } if (err) { callback(err); return; }
console.log(ifname);
self.exec('dhcpserver remove --ifname ' + ifname, function (err, stdout, stderr) { self.exec('dhcpserver remove --ifname ' + ifname, function (err, stdout, stderr) {
callback(err); callback(err);
}); });
}); });
}; };

View File

@ -69,10 +69,10 @@ Meteor.call('getDockerHost', function (err, host) {
}); });
fixBoot2DockerVM = function (callback) { fixBoot2DockerVM = function (callback) {
checkBoot2DockerVM(function (err) { Boot2Docker.check(function (err) {
if (err) { if (err) {
Session.set('available', false); Session.set('available', false);
resolveBoot2DockerVM(function (err) { Boot2Docker.resolve(function (err) {
if (err) { if (err) {
callback(err); callback(err);
} else { } else {
@ -145,13 +145,12 @@ Meteor.setInterval(function () {
}); });
}, 5000); }, 5000);
fixInterval = null; Meteor.setInterval(function () {
startFixInterval = function () { if (Installs.findOne()) {
stopFixInterval();
fixInterval = Meteor.setInterval(function () {
resolveWatchers(function () {}); resolveWatchers(function () {});
fixBoot2DockerVM(function (err) { fixBoot2DockerVM(function (err) {
if (err) { console.log(err); return; } if (err) { console.log(err); return; }
Meteor.call('recoverApps');
fixDefaultImages(function (err) { fixDefaultImages(function (err) {
if (err) { console.log(err); return; } if (err) { console.log(err); return; }
fixDefaultContainers(function (err) { fixDefaultContainers(function (err) {
@ -159,10 +158,6 @@ startFixInterval = function () {
}); });
}); });
}); });
}
}, 5000); }, 5000);
};
stopFixInterval = function () {
Meteor.clearInterval(fixInterval);
fixInterval = null;
};

View File

@ -21,8 +21,8 @@ Template.dashboard_single_app.events({
}, },
'click .btn-terminal': function () { 'click .btn-terminal': function () {
var app = this; var app = this;
var cmd = path.join(getBinDir(), 'boot2docker') + ' ssh -t "sudo docker-enter ' + app.docker.Id + '"'; var cmd = path.join(Util.getBinDir(), 'boot2docker') + ' ssh -t "sudo docker-enter ' + app.docker.Id + '"';
var terminalCmd = path.join(getBinDir(), 'terminal') + ' ' + cmd; var terminalCmd = path.join(Util.getBinDir(), 'terminal') + ' ' + cmd;
var exec = require('child_process').exec; var exec = require('child_process').exec;
console.log(terminalCmd); console.log(terminalCmd);
exec(terminalCmd, function (err, stdout) { exec(terminalCmd, function (err, stdout) {

View File

@ -8,7 +8,7 @@
{{#if $.Session.equals 'boot2dockerState' 'poweroff'}} {{#if $.Session.equals 'boot2dockerState' 'poweroff'}}
<p class="help-block error">Please start Boot2Docker for Kitematic to work properly!</p> <p class="help-block error">Please start Boot2Docker for Kitematic to work properly!</p>
{{else}} {{else}}
<p class="help-block">All apps run in a Linux VM included with Kitematic. It needs to be turned to run apps.</p> <p class="help-block">All apps run in a Linux VM included with Kitematic. It needs to be turned on to run apps.</p>
{{/if}} {{/if}}
</div> </div>
<div class="right-section"> <div class="right-section">

View File

@ -8,7 +8,6 @@ Template.setup_install.rendered = function() {
console.log(err); console.log(err);
} else { } else {
Installs.insert({}); Installs.insert({});
startFixInterval();
Router.go('dashboard_apps'); Router.go('dashboard_apps');
} }
}); });

View File

@ -116,3 +116,41 @@ Apps.helpers({
}); });
Apps.attachSchema(schemaApps); 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.');
});
});

View File

@ -91,3 +91,43 @@ Images.allow({
}); });
Images.attachSchema(schemaImages); 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);
}
});

146
meteor/lib/utilities.js Normal file
View File

@ -0,0 +1,146 @@
Util = {};
Util.getHomePath = function () {
return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
};
Util.getBinDir = function () {
if (process.env.NODE_ENV === 'development') {
return path.join(path.join(process.env.PWD, '..'), 'resources');
} else {
if (Meteor.isClient) {
return path.join(process.cwd(), 'resources');
} else {
return path.join(process.cwd(), '../../../resources');
}
}
};
Util.deleteFolder = function (directory) {
if (fs.existsSync(directory)) {
fs.readdirSync(directory).forEach(function (file) {
var curDirectory = directory + '/' + file;
if (fs.lstatSync(curDirectory).isDirectory()) {
// Recurse
Util.deleteFolder(curDirectory);
} else {
// Delete File
try {
fs.unlinkSync(curDirectory);
} catch (e) {
console.error(e);
}
}
});
fs.rmdirSync(directory);
}
};
Util.copyFolder = function (src, dest) {
var exists = fs.existsSync(src);
var stats = exists && fs.statSync(src);
var isDirectory = exists && stats.isDirectory();
if (exists && isDirectory) {
try {
fs.mkdirSync(dest);
} catch (e) {
console.error(e);
}
fs.readdirSync(src).forEach(function (childItemName) {
Util.copyFolder(path.join(src, childItemName), path.join(dest, childItemName));
});
} else {
try {
fs.linkSync(src, dest);
} catch (e) {
console.error(e);
}
}
};
Util.copyVolumes = function (directory, appName) {
var KITE_VOLUMES_PATH = path.join(directory, 'volumes');
if (fs.existsSync(KITE_VOLUMES_PATH)) {
var destinationPath = path.join(KITE_PATH, appName);
Util.copyFolder(KITE_VOLUMES_PATH, destinationPath);
console.log('Copied volumes for: ' + appName);
}
};
Util.hasDockerfile = function (directory) {
return fs.existsSync(path.join(directory, 'Dockerfile'));
};
/**
* Compares two software version numbers (e.g. "1.7.1" or "1.2b").
*
* @param {string} v1 The first version to be compared.
* @param {string} v2 The second version to be compared.
* @param {object} [options] Optional flags that affect comparison behavior:
* <ul>
* <li>
* <tt>lexicographical: true</tt> compares each part of the version strings lexicographically instead of
* naturally; this allows suffixes such as "b" or "dev" but will cause "1.10" to be considered smaller than
* "1.2".
* </li>
* <li>
* <tt>zeroExtend: true</tt> changes the result if one version string has less parts than the other. In
* this case the shorter string will be padded with "zero" parts instead of being considered smaller.
* </li>
* </ul>
* @returns {number|NaN}
* <ul>
* <li>0 if the versions are equal</li>
* <li>a negative integer iff v1 < v2</li>
* <li>a positive integer iff v1 > v2</li>
* <li>NaN if either version string is in the wrong format</li>
* </ul>
*
*/
Util.compareVersions = function (v1, v2, options) {
var lexicographical = options && options.lexicographical,
zeroExtend = options && options.zeroExtend,
v1parts = v1.split('.'),
v2parts = v2.split('.');
function isValidPart(x) {
return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
}
if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
return NaN;
}
if (zeroExtend) {
while (v1parts.length < v2parts.length) v1parts.push('0');
while (v2parts.length < v1parts.length) v2parts.push('0');
}
if (!lexicographical) {
v1parts = v1parts.map(Number);
v2parts = v2parts.map(Number);
}
for (var i = 0; i < v1parts.length; ++i) {
if (v2parts.length == i) {
return 1;
}
if (v1parts[i] == v2parts[i]) {
continue;
}
else if (v1parts[i] > v2parts[i]) {
return 1;
}
else {
return -1;
}
}
if (v1parts.length != v2parts.length) {
return -1;
}
return 0;
};

View File

@ -1,10 +1,55 @@
removeBindFolder = function (name, callback) { Apps.restart = function (app, callback) {
exec(path.join(getBinDir(), 'boot2docker') + ' ssh "sudo rm -rf /var/lib/docker/binds/' + name + '"', function(err, stdout) { if (app.docker && app.docker.Id) {
callback(err, stdout); try {
}); Docker.restartContainerSync(app.docker.Id);
} catch (e) {
console.error(e);
}
var containerData = Docker.getContainerDataSync(app.docker.Id);
Fiber(function () {
Apps.update(app._id, {$set: {
status: 'READY',
docker: containerData
}});
}).run();
callback(null);
// Use dig to refresh the DNS
exec('/usr/bin/dig dig ' + app.name + '.dev @172.17.42.1 ', function() {});
} else {
callback(null);
}
}; };
recoverApps = function (callback) { Apps.logs = function (app) {
if (app.docker && app.docker.Id) {
var container = docker.getContainer(app.docker.Id);
container.logs({follow: false, stdout: true, stderr: true, timestamps: true, tail: 300}, function (err, response) {
if (err) { throw err; }
Fiber(function () {
Apps.update(app._id, {
$set: {
logs: []
}
});
}).run();
var logs = [];
response.setEncoding('utf8');
response.on('data', function (line) {
logs.push(convert.toHtml(line.slice(8)));
Fiber(function () {
Apps.update(app._id, {
$set: {
logs: logs
}
});
}).run();
});
response.on('end', function () {});
});
}
};
Apps.recover = function (callback) {
var apps = Apps.find({}).fetch(); var apps = Apps.find({}).fetch();
_.each(apps, function (app) { _.each(apps, function (app) {
// Update the app with the latest container info // Update the app with the latest container info
@ -17,7 +62,7 @@ recoverApps = function (callback) {
console.log('restarting: ' + app.name); console.log('restarting: ' + app.name);
console.log(app.docker.Id); console.log(app.docker.Id);
Fiber(function () { Fiber(function () {
restartApp(app, function (err) { Apps.restart(app, function (err) {
if (err) { console.error(err); } if (err) { console.error(err); }
}); });
}).run(); }).run();
@ -29,7 +74,8 @@ recoverApps = function (callback) {
Meteor.methods({ Meteor.methods({
recoverApps: function () { recoverApps: function () {
return Meteor._wrapAsync(recoverApps)(); this.unblock();
return Meteor._wrapAsync(Apps.recover)();
}, },
configVar: function (appId, configVars) { configVar: function (appId, configVars) {
this.unblock(); this.unblock();
@ -48,64 +94,38 @@ Meteor.methods({
if (!app) { if (!app) {
throw new Meteor.Error(403, 'No app found with this ID'); throw new Meteor.Error(403, 'No app found with this ID');
} }
deleteApp(app, function (err) {
if (err) { console.error(err); }
var appPath = path.join(KITE_PATH, app.name);
deleteFolder(appPath);
removeBindFolder(app.name, function () {
console.log('Deleted Kite ' + app.name + ' directory.');
Fiber(function () {
Apps.remove({_id: app._id}); Apps.remove({_id: app._id});
}).run();
});
});
}, },
createApp: function (formData) { createApp: function (formData) {
this.unblock();
var validationResult = formValidate(formData, FormSchema.formCreateApp); var validationResult = formValidate(formData, FormSchema.formCreateApp);
if (validationResult.errors) { if (validationResult.errors) {
throw new Meteor.Error(400, 'Validation Failed.', validationResult.errors); throw new Meteor.Error(400, 'Validation Failed.', validationResult.errors);
} else { } else {
var cleaned = validationResult.cleaned; var cleaned = validationResult.cleaned;
var appObj = { var appName = cleaned.name;
name: cleaned.name, var appPath = path.join(KITE_PATH, appName);
imageId: cleaned.imageId,
status: 'STARTING',
config: {}
};
var appId = Apps.insert(appObj);
var appPath = path.join(KITE_PATH, appObj.name);
if (!fs.existsSync(appPath)) { if (!fs.existsSync(appPath)) {
console.log('Created Kite ' + appObj.name + ' directory.'); console.log('Created Kite ' + appName + ' directory.');
fs.mkdirSync(appPath, function (err) { fs.mkdirSync(appPath, function (err) {
if (err) { throw err; } if (err) { throw err; }
}); });
} }
Apps.update(appId, { var appObj = {
$set: { name: appName,
'config.APP_ID': appId, imageId: cleaned.imageId,
status: 'STARTING',
config: {},
path: appPath path: appPath
} };
}); Apps.insert(appObj);
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) { getAppLogs: function (appId) {
this.unblock(); this.unblock();
var app = Apps.findOne(appId); var app = Apps.findOne(appId);
if (app) { if (app) {
getAppLogs(app, function (err) { Apps.logs(app, function (err) {
if (err) { throw err; } if (err) { throw err; }
}); });
} }
@ -117,12 +137,13 @@ Meteor.methods({
Apps.update(app._id, {$set: { Apps.update(app._id, {$set: {
status: 'STARTING' status: 'STARTING'
}}); }});
restartApp(app, function (err) { Apps.restart(app, function (err) {
if (err) { console.error(err); } if (err) { console.error(err); }
}); });
} }
}, },
resolveWatchers: function () { resolveWatchers: function () {
this.unblock();
return Meteor._wrapAsync(resolveWatchers)(); return Meteor._wrapAsync(resolveWatchers)();
} }
}); });

View File

@ -0,0 +1,24 @@
KITE_PATH = path.join(Util.getHomePath(), 'Kitematic');
KITE_TAR_PATH = path.join(KITE_PATH, '.tar');
KITE_IMAGES_PATH = path.join(KITE_PATH, '.images');
if (!fs.existsSync(KITE_PATH)) {
console.log('Created Kitematic directory.');
fs.mkdirSync(KITE_PATH, function (err) {
if (err) { throw err; }
});
}
if (!fs.existsSync(KITE_TAR_PATH)) {
console.log('Created Kitematic .tar directory.');
fs.mkdirSync(KITE_TAR_PATH, function (err) {
if (err) { throw err; }
});
}
if (!fs.existsSync(KITE_IMAGES_PATH)) {
console.log('Created Kitematic .images directory.');
fs.mkdirSync(KITE_IMAGES_PATH, function (err) {
if (err) { throw err; }
});
}

View File

@ -1,16 +1,11 @@
Docker = Meteor.require('dockerode'); Dockerode = Meteor.require('dockerode');
var Convert = Meteor.require('ansi-to-html');
var convert = new Convert();
var DOCKER_HOST='192.168.60.103'; var DOCKER_HOST='192.168.60.103';
docker = new Docker({host: DOCKER_HOST, port: '2375'}); docker = new Dockerode({host: DOCKER_HOST, port: '2375'});
hasDockerfile = function (directory) { Docker = {};
return fs.existsSync(path.join(directory, 'Dockerfile'));
};
removeContainer = function (containerId, callback) { Docker.removeContainer = function (containerId, callback) {
var container = docker.getContainer(containerId); var container = docker.getContainer(containerId);
container.kill(function (err) { container.kill(function (err) {
if (err) { callback(err); return; } if (err) { callback(err); return; }
@ -22,28 +17,11 @@ removeContainer = function (containerId, callback) {
}); });
}; };
removeContainerSync = function (containerId) { Docker.removeContainerSync = function (containerId) {
return Meteor._wrapAsync(removeContainer)(containerId); return Meteor._wrapAsync(Docker.removeContainer)(containerId);
}; };
deleteApp = function (app, callback) { Docker.getContainerData = function (containerId, 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); var container = docker.getContainer(containerId);
container.inspect(function (err, data) { container.inspect(function (err, data) {
if (err) { if (err) {
@ -59,11 +37,11 @@ getContainerData = function (containerId, callback) {
}); });
}; };
getContainerDataSync = function (containerId) { Docker.getContainerDataSync = function (containerId) {
return Meteor._wrapAsync(getContainerData)(containerId); return Meteor._wrapAsync(Docker.getContainerData)(containerId);
}; };
runContainer = function (app, image, callback) { Docker.runContainer = function (app, image, callback) {
var envParam = []; var envParam = [];
_.each(_.keys(app.config), function (key) { _.each(_.keys(app.config), function (key) {
var builtStr = key + '=' + app.config[key]; var builtStr = key + '=' + app.config[key];
@ -100,11 +78,11 @@ runContainer = function (app, image, callback) {
}); });
}; };
runContainerSync = function (app, image) { Docker.runContainerSync = function (app, image) {
return Meteor._wrapAsync(runContainer)(app, image); return Meteor._wrapAsync(Docker.runContainer)(app, image);
}; };
restartContainer = function (containerId, callback) { Docker.restartContainer = function (containerId, callback) {
var container = docker.getContainer(containerId); var container = docker.getContainer(containerId);
container.restart(function (err) { container.restart(function (err) {
if (err) { if (err) {
@ -117,83 +95,8 @@ restartContainer = function (containerId, callback) {
}); });
}; };
restartContainerSync = function (containerId) { Docker.restartContainerSync = function (containerId) {
return Meteor._wrapAsync(restartContainer)(containerId); return Meteor._wrapAsync(Docker.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) { var convertVolumeObjToArray = function (obj) {
@ -209,7 +112,7 @@ var convertVolumeObjToArray = function (obj) {
return result; return result;
}; };
getImageData = function (imageId, callback) { Docker.getImageData = function (imageId, callback) {
var image = docker.getImage(imageId.toLowerCase()); var image = docker.getImage(imageId.toLowerCase());
image.inspect(function (err, data) { image.inspect(function (err, data) {
if (err) { if (err) {
@ -224,11 +127,11 @@ getImageData = function (imageId, callback) {
}); });
}; };
getImageDataSync = function (imageId) { Docker.getImageDataSync = function (imageId) {
return Meteor._wrapAsync(getImageData)(imageId); return Meteor._wrapAsync(Docker.getImageData)(imageId);
}; };
removeImage = function (imageId, callback) { Docker.removeImage = function (imageId, callback) {
var image = docker.getImage(imageId.toLowerCase()); var image = docker.getImage(imageId.toLowerCase());
image.remove({force: true}, function (err) { image.remove({force: true}, function (err) {
if (err) { callback(err); return; } if (err) { callback(err); return; }
@ -237,25 +140,14 @@ removeImage = function (imageId, callback) {
}); });
}; };
removeImageSync = function (imageId) { Docker.removeImageSync = function (imageId) {
return Meteor._wrapAsync(removeImage)(imageId); return Meteor._wrapAsync(Docker.removeImage)(imageId);
}; };
deleteImage = function (image, callback) { Docker.removeBindFolder = function (name, callback) {
if (!image.docker) { exec(path.join(Util.getBinDir(), 'boot2docker') + ' ssh "sudo rm -rf /var/lib/docker/binds/' + name + '"', function (err, stdout) {
callback(null, {}); callback(err, stdout);
return; });
}
try {
removeImageSync(image.docker.Id);
} catch (e) {
console.error(e);
}
callback(null);
};
deleteImageSync = function (image) {
return Meteor._wrapAsync(deleteImage)(image);
}; };
var defaultContainerOptions = function () { var defaultContainerOptions = function () {
@ -304,7 +196,7 @@ resolveDefaultImages = function () {
image.inspect(function (err) { image.inspect(function (err) {
if (err) { if (err) {
if (err.reason === 'no such image') { if (err.reason === 'no such image') {
docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) { docker.loadImage(path.join(Util.getBinDir(), 'base-images.tar.gz'), {}, function (err) {
if (err) { if (err) {
innerCallback(err); innerCallback(err);
return; return;
@ -374,7 +266,7 @@ reloadDefaultContainers = function (callback) {
async.until(function () { async.until(function () {
return ready; return ready;
}, function (callback) { }, function (callback) {
docker.listContainers(function (err, containers) { docker.listContainers(function (err) {
if (!err) { if (!err) {
ready = true; ready = true;
} }
@ -391,7 +283,7 @@ reloadDefaultContainers = function (callback) {
return; return;
} }
console.log('Loading new Kitematic default images.'); console.log('Loading new Kitematic default images.');
docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) { docker.loadImage(path.join(Util.getBinDir(), 'base-images.tar.gz'), {}, function (err) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
@ -500,142 +392,18 @@ killAndRemoveContainers = function (names, callback) {
}); });
}; };
pullImageFromDockerfile = function (dockerfile, imageId, callback) {
var fromImage = getFromImage(dockerfile);
console.log('From image: ' + fromImage);
var installedImage = null;
try {
installedImage = getImageDataSync(fromImage);
} catch (e) {
console.error(e);
}
if (fromImage && !installedImage) {
Fiber(function () {
Images.update(imageId, {
$set: {
buildLogs: []
}
});
}).run();
var logs = [];
docker.pull(fromImage, function (err, response) {
if (err) { callback(err); return; }
response.setEncoding('utf8');
response.on('data', function (data) {
try {
var logData = JSON.parse(data);
var logDisplay = '';
if (logData.id) {
logDisplay += logData.id + ' | ';
}
logDisplay += logData.status;
if (logData.progressDetail && logData.progressDetail.current && logData.progressDetail.total) {
logDisplay += ' - ' + Math.round(logData.progressDetail.current / logData.progressDetail.total * 100) + '%';
}
logs.push(logDisplay);
Fiber(function () {
Images.update(imageId, {
$set: {
buildLogs: logs
}
});
}).run();
} catch (e) {
console.error(e);
}
});
response.on('end', function () {
console.log('Finished pulling image: ' + fromImage);
callback(null);
});
});
} else {
callback(null);
}
};
buildImage = function (image, callback) {
Fiber(function () {
var tarFilePath = createTarFileSync(image);
Images.update(image._id, {
$set: {
buildLogs: []
}
});
docker.buildImage(tarFilePath, {t: image._id.toLowerCase()}, function (err, response) {
if (err) { callback(err); }
console.log('Building Docker image...');
var logs = [];
response.setEncoding('utf8');
response.on('data', function (data) {
try {
var line = JSON.parse(data).stream;
logs.push(convert.toHtml(line));
Fiber(function () {
Images.update(image._id, {
$set: {
buildLogs: logs
}
});
}).run();
} catch (e) {
console.error(e);
}
});
response.on('end', function () {
console.log('Finished building Docker image.');
try {
fs.unlinkSync(tarFilePath);
console.log('Cleaned up tar file.');
} catch (e) {
console.error(e);
}
Fiber(function () {
var imageData = null;
try {
imageData = getImageDataSync(image._id);
Images.update(image._id, {
$set: {
docker: imageData,
status: 'READY'
}
});
} catch (e) {
console.log(e);
Images.update(image._id, {
$set: {
status: 'ERROR'
}
});
}
var oldImageId = null;
if (image.docker && image.docker.Id) {
oldImageId = image.docker.Id;
}
if (oldImageId && imageData && oldImageId !== imageData.Id) {
try {
removeImageSync(oldImageId);
} catch (e) {
console.error(e);
}
}
}).run();
callback(null);
});
});
}).run();
};
Meteor.methods({ Meteor.methods({
runApp: function (app) { runApp: function (app) {
this.unblock(); this.unblock();
var image = Images.findOne({_id: app.imageId}); var image = Images.findOne({_id: app.imageId});
// Delete old container if one already exists
try { try {
removeContainerSync(app.name); Docker.removeContainerSync(app.name);
} catch (e) {} } catch (e) {}
try { try {
var container = runContainerSync(app, image); var container = Docker.runContainerSync(app, image);
var containerData = getContainerDataSync(container.id); var containerData = Docker.getContainerDataSync(container.id);
// Set a delay for app to spin up
Meteor.setTimeout(function () { Meteor.setTimeout(function () {
Apps.update(app._id, {$set: { Apps.update(app._id, {$set: {
docker: containerData, docker: containerData,
@ -650,18 +418,23 @@ Meteor.methods({
return DOCKER_HOST; return DOCKER_HOST;
}, },
reloadDefaultContainers: function () { reloadDefaultContainers: function () {
this.unblock();
return Meteor._wrapAsync(reloadDefaultContainers)(); return Meteor._wrapAsync(reloadDefaultContainers)();
}, },
checkDefaultImages: function () { checkDefaultImages: function () {
this.unblock();
return Meteor._wrapAsync(checkDefaultImages)(); return Meteor._wrapAsync(checkDefaultImages)();
}, },
resolveDefaultImages: function () { resolveDefaultImages: function () {
this.unblock();
return Meteor._wrapAsync(resolveDefaultImages)(); return Meteor._wrapAsync(resolveDefaultImages)();
}, },
checkDefaultContainers: function () { checkDefaultContainers: function () {
this.unblock();
return Meteor._wrapAsync(checkDefaultContainers)(); return Meteor._wrapAsync(checkDefaultContainers)();
}, },
resolveDefaultContainers: function () { resolveDefaultContainers: function () {
this.unblock();
return Meteor._wrapAsync(resolveDefaultContainers)(); return Meteor._wrapAsync(resolveDefaultContainers)();
} }
}); });

View File

@ -1,4 +1,38 @@
getImageMetaData = function (directory) { var createTarFile = function (image, callback) {
var TAR_PATH = path.join(KITE_TAR_PATH, image._id + '.tar');
exec('tar czf ' + TAR_PATH + ' -C ' + image.path + ' .', function (err) {
if (err) { callback(err, null); return; }
console.log('Created tar file: ' + TAR_PATH);
callback(null, TAR_PATH);
});
};
var createTarFileSync = function (image) {
return Meteor._wrapAsync(createTarFile)(image);
};
var getFromImage = function (dockerfile) {
var patternString = "(FROM)(.*)";
var regex = new RegExp(patternString, "g");
var fromInstruction = dockerfile.match(regex);
if (fromInstruction && fromInstruction.length > 0) {
return fromInstruction[0].replace('FROM', '').trim();
} else {
return null;
}
};
var getImageJSON = function (directory) {
var KITE_JSON_PATH = path.join(directory, 'image.json');
if (fs.existsSync(KITE_JSON_PATH)) {
var data = fs.readFileSync(KITE_JSON_PATH, 'utf8');
return JSON.parse(data);
} else {
return null;
}
};
var getImageMetaData = function (directory) {
var kiteJSON = getImageJSON(directory); var kiteJSON = getImageJSON(directory);
if (kiteJSON) { if (kiteJSON) {
if (!kiteJSON.name) { if (!kiteJSON.name) {
@ -12,8 +46,24 @@ getImageMetaData = function (directory) {
return kiteJSON; return kiteJSON;
}; };
rebuildImage = function (image, callback) { Images.saveFolder = function (directory, imageId, callback) {
deleteFolder(image.path); 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); var imageMetaData = getImageMetaData(image.originPath);
if (imageMetaData.logo) { if (imageMetaData.logo) {
Images.update(image._id, { Images.update(image._id, {
@ -35,18 +85,144 @@ rebuildImage = function (image, callback) {
} }
}); });
image = Images.findOne(image._id); image = Images.findOne(image._id);
saveImageFolderSync(image.originPath, image._id); Images.saveFolderSync(image.originPath, image._id);
pullImageFromDockerfile(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) { Images.pull(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) {
if (err) { callback(err, null); return; } if (err) { callback(err, null); return; }
buildImage(image, function (err) { Images.build(image, function (err) {
if (err) { console.error(err); } if (err) { console.error(err); }
callback(null, null); callback(null, null);
}); });
}); });
}; };
rebuildImageSync = function (image) { Images.rebuildSync = function (image) {
return Meteor._wrapAsync(rebuildImage)(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({ Meteor.methods({
@ -58,29 +234,7 @@ Meteor.methods({
}; };
var imageMetaData = getImageMetaData(directory); var imageMetaData = getImageMetaData(directory);
imageObj.meta = imageMetaData; imageObj.meta = imageMetaData;
var imageId = Images.insert(imageObj); Images.insert(imageObj);
var imagePath = path.join(KITE_IMAGES_PATH, imageId);
Images.update(imageId, {
$set: {
path: imagePath
}
});
if (imageObj.meta.logo) {
Images.update(imageId, {
$set: {
logoPath: path.join(imagePath, imageObj.meta.logo)
}
});
}
var image = Images.findOne(imageId);
saveImageFolderSync(directory, imageId);
console.log('Saved folder sync');
pullImageFromDockerfile(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), imageId, function (err) {
if (err) { throw err; }
buildImage(image, function (err) {
if (err) { console.error(err); }
});
});
}, },
rebuildImage: function (imageId) { rebuildImage: function (imageId) {
this.unblock(); this.unblock();
@ -92,7 +246,13 @@ Meteor.methods({
if (apps.length > 0) { if (apps.length > 0) {
_.each(apps, function (app) { _.each(apps, function (app) {
console.log('Updating app: ' + app.name); console.log('Updating app: ' + app.name);
deleteAppSync(app); if (app.docker) {
try {
Docker.removeContainerSync(app.docker.Id);
} catch (e) {
console.error(e);
}
}
Apps.update(app._id, { Apps.update(app._id, {
$set: { $set: {
'docker.Id': null, 'docker.Id': null,
@ -101,7 +261,7 @@ Meteor.methods({
} }
}); });
}); });
rebuildImageSync(image); Images.rebuildSync(image);
_.each(apps, function (app) { _.each(apps, function (app) {
app = Apps.findOne(app._id); app = Apps.findOne(app._id);
Meteor.call('runApp', app, function (err) { Meteor.call('runApp', app, function (err) {
@ -109,7 +269,7 @@ Meteor.methods({
}); });
}); });
} else { } else {
rebuildImageSync(image); Images.rebuildSync(image);
} }
}, },
changeDirectory: function (imageId, directory) { changeDirectory: function (imageId, directory) {
@ -125,7 +285,8 @@ Meteor.methods({
}); });
}, },
validateDirectory: function (directory) { validateDirectory: function (directory) {
if (!hasDockerfile(directory)) { this.unblock();
if (!Util.hasDockerfile(directory)) {
throw new Meteor.Error(400, "Only directories with Dockerfiles are supported now."); throw new Meteor.Error(400, "Only directories with Dockerfiles are supported now.");
} }
}, },
@ -137,15 +298,7 @@ Meteor.methods({
} }
var app = Apps.findOne({imageId: imageId}); var app = Apps.findOne({imageId: imageId});
if (!app) { if (!app) {
console.log('here');
try {
deleteImageSync(image);
deleteFolder(image.path);
} catch (e) {
console.log(e);
} finally {
Images.remove({_id: image._id}); Images.remove({_id: image._id});
}
} else { } else {
throw new Meteor.Error(400, 'This image is currently being used by <a href="/apps/' + app.name + '">' + app.name + "</a>."); throw new Meteor.Error(400, 'This image is currently being used by <a href="/apps/' + app.name + '">' + app.name + "</a>.");
} }

View File

@ -1,59 +0,0 @@
KITE_PATH = path.join(getHomePath(), 'Kitematic');
KITE_TAR_PATH = path.join(KITE_PATH, '.tar');
KITE_IMAGES_PATH = path.join(KITE_PATH, '.images');
if (!fs.existsSync(KITE_PATH)) {
console.log('Created Kitematic directory.');
fs.mkdirSync(KITE_PATH, function (err) {
if (err) { throw err; }
});
}
if (!fs.existsSync(KITE_TAR_PATH)) {
console.log('Created Kitematic .tar directory.');
fs.mkdirSync(KITE_TAR_PATH, function (err) {
if (err) { throw err; }
});
}
if (!fs.existsSync(KITE_IMAGES_PATH)) {
console.log('Created Kitematic .images directory.');
fs.mkdirSync(KITE_IMAGES_PATH, function (err) {
if (err) { throw err; }
});
}
getImageJSON = function (directory) {
var KITE_JSON_PATH = path.join(directory, 'image.json');
if (fs.existsSync(KITE_JSON_PATH)) {
var data = fs.readFileSync(KITE_JSON_PATH, 'utf8');
return JSON.parse(data);
} else {
return null;
}
};
loadKiteVolumes = function (directory, appName) {
var KITE_VOLUMES_PATH = path.join(directory, 'volumes');
if (fs.existsSync(KITE_VOLUMES_PATH)) {
var destinationPath = path.join(KITE_PATH, appName);
copyFolder(KITE_VOLUMES_PATH, destinationPath);
console.log('Copied volumes for: ' + appName);
}
};
saveImageFolder = function (directory, imageId, callback) {
var destinationPath = path.join(KITE_IMAGES_PATH, imageId);
if (!fs.existsSync(destinationPath)) {
fs.mkdirSync(destinationPath, function (err) {
if (err) { callback(err); return; }
});
copyFolder(directory, destinationPath);
console.log('Copied image folder for: ' + imageId);
callback(null);
}
};
saveImageFolderSync = function (directory, imageId) {
return Meteor._wrapAsync(saveImageFolder)(directory, imageId);
};

View File

@ -7,3 +7,5 @@ exec = Meteor.require('child_process').exec;
async = Meteor.require('async'); async = Meteor.require('async');
Fiber = Meteor.require('fibers'); Fiber = Meteor.require('fibers');
child_process = Meteor.require('child_process'); child_process = Meteor.require('child_process');
Convert = Meteor.require('ansi-to-html');
convert = new Convert();

View File

@ -1,53 +0,0 @@
getHomePath = function () {
return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
};
getBinDir = function () {
if (process.env.NODE_ENV === 'development') {
return path.join(path.join(process.env.PWD, '..'), 'resources');
} else {
return path.join(process.cwd(), '../../../resources');
}
};
deleteFolder = function (directory) {
if (fs.existsSync(directory)) {
fs.readdirSync(directory).forEach(function (file) {
var curDirectory = directory + '/' + file;
if (fs.lstatSync(curDirectory).isDirectory()) {
// Recurse
deleteFolder(curDirectory);
} else {
// Delete File
try {
fs.unlinkSync(curDirectory);
} catch (e) {
console.error(e);
}
}
});
fs.rmdirSync(directory);
}
};
copyFolder = function (src, dest) {
var exists = fs.existsSync(src);
var stats = exists && fs.statSync(src);
var isDirectory = exists && stats.isDirectory();
if (exists && isDirectory) {
try {
fs.mkdirSync(dest);
} catch (e) {
console.error(e);
}
fs.readdirSync(src).forEach(function (childItemName) {
copyFolder(path.join(src, childItemName), path.join(dest, childItemName));
});
} else {
try {
fs.linkSync(src, dest);
} catch (e) {
console.error(e);
}
}
};

View File

@ -7,5 +7,5 @@ Meteor.publish('images', function () {
}); });
Meteor.publish('installs', function () { Meteor.publish('installs', function () {
return Installs.find({}, {sort: {createdAt: -1}}); return Installs.find({});
}); });

View File

@ -28,9 +28,6 @@ echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<string>172.17.0.0</string> <string>172.17.0.0</string>
<string>-netmask</string> <string>-netmask</string>
<string>255.255.0.0</string> <string>255.255.0.0</string>
<string>-iface</string>
<string>$IFNAME</string>
<string>255.255.0.0</string>
<string>-gateway</string> <string>-gateway</string>
<string>$GATEWAY</string> <string>$GATEWAY</string>
</array> </array>
@ -45,4 +42,4 @@ echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
# Add entries to routing table for Kitematic VM # Add entries to routing table for Kitematic VM
/sbin/route delete -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY /sbin/route delete -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY
/sbin/route -n add -net 172.17.0.0 -netmask 255.255.0.0 -iface $IFNAME -gateway $GATEWAY /sbin/route -n add -net 172.17.0.0 -netmask 255.255.0.0 -gateway $GATEWAY