diff --git a/index.html b/index.html index a97ba8b788..94d57f9b1e 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ Kitematic - + diff --git a/package.json b/package.json index 6451447e46..3618b083e5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "bugs": "https://github.com/kitematic/kitematic/issues", "scripts": { - "start": "rsync ./index.html ./build/ && rsync ./fonts/* ./build/ && rsync ./images/* ./build && jsx --watch src/ build/ & wess -w -m -i ./styles/main.less -o ./build/main.css & wiper -w ./build/**/*.* & ./cache/Atom.app/Contents/MacOS/Atom .", + "start": "rsync ./index.html ./build/ && rsync ./fonts/* ./build/ && rsync ./images/* ./build && jsx --watch src/ build/ & wess -w -m -i ./styles/main.less -o ./build/main.css & wiper -w ./build/**/*.* & ./cache/Atom.app/Contents/MacOS/Atom . && kill $(pgrep node)", "preinstall": "./deps", "test": "jest", "release": "./release" @@ -33,7 +33,7 @@ "dependencies": { "ansi-to-html": "0.2.0", "async": "^0.9.0", - "bugsnag-js": "git+https://git@github.com/bugsnag/bugsnag-js", + "bugsnag-js": "^2.4.6", "dockerode": "2.0.4", "exec": "0.1.2", "gulp-react": "^2.0.0", diff --git a/src/Boot2Docker.js b/src/Boot2Docker.js new file mode 100644 index 0000000000..8e64baedc4 --- /dev/null +++ b/src/Boot2Docker.js @@ -0,0 +1,224 @@ +var exec = require('exec'); +var path = require('path'); +var fs = require('fs'); +var path = require('path'); +var async = require('async'); + +var cmdExec = function (cmd, callback) { + exec(cmd, function (stderr, stdout, code) { + if (code !== 0) { + callback('Exit code ' + code + ': ' + stderr); + } else { + callback(null, stdout); + } + }); +}; + +var homeDir = function () { + return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; +}; + +var Boot2Docker = { + version: function () { + return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'))['boot2docker-version']; + }, + cliVersion: function (callback) { + cmdExec([Boot2Docker.command(), 'version'], function (err, out) { + if (err) { + callback(err); + return; + } + var match = out.match(/version: v(\d+\.\d+\.\d+)/); + if (!match || match.length < 2) { + callback('Could not parse the boot2docker cli version.'); + } else { + callback(null, match[1]); + } + }); + }, + isoVersion: function (callback) { + fs.readFile(path.join(homeDir(), '.boot2docker', 'boot2docker.iso'), 'utf8', function (err, data) { + if (err) { + callback(err); + return; + } + var match = data.match(/Boot2Docker-v(\d+\.\d+\.\d+)/); + if (!match) { + callback('Could not parse boot2docker iso version'); + return; + } + callback (null, match[1]); + }); + }, + command: function () { + return path.join(process.cwd(), 'resources', 'boot2docker-' + this.version()); + }, + exists: function (callback) { + cmdExec([Boot2Docker.command(), 'info'], function (err, out) { + if (err) { + callback(null, false); + } else { + callback(null, true); + } + }); + }, + status: function (callback) { + cmdExec([Boot2Docker.command(), 'status'], function (err, out) { + if (err) { + callback(err); + return; + } + callback(null, out.trim()); + }); + }, + init: function (callback) { + cmdExec([Boot2Docker.command(), 'init'], callback); + }, + start: function (callback) { + cmdExec([Boot2Docker.command(), 'start'], callback); + }, + stop: function (callback) { + cmdExec([Boot2Docker.command(), 'stop'], callback); + }, + upgrade: function (callback) { + cmdExec([Boot2Docker.command(), 'upgrade'], callback); + }, + ip: function (callback) { + cmdExec([Boot2Docker.command(), 'ip'], callback); + }, + erase: function (callback) { + var VMFileLocation = path.join(homeDir(), 'VirtualBox\\ VMs/boot2docker-vm'); + cmdExec(['rm', '-rf', VMFileLocation], callback); + }, + state: function (callback) { + cmdExec([Boot2Docker.command(), 'info'], function (err, out) { + if (err) { + callback(err); + return; + } + try { + var info = JSON.parse(out); + callback(null, info.State); + } catch (e) { + callback(e, null); + } + }); + }, + disk: function (callback) { + cmdExec([Boot2Docker.command(), 'ssh', 'df'], function (err, out) { + if (err) { + callback(err); + return; + } + try { + var lines = out.split('\n'); + var dataline = _.find(lines, function (line) { + return line.indexOf('/dev/sda1') !== -1; + }); + var tokens = dataline.split(' '); + tokens = tokens.filter(function (token) { + return token !== ''; + }); + var usedGb = parseInt(tokens[2], 10) / 1000000; + var totalGb = parseInt(tokens[3], 10) / 1000000; + var percent = parseInt(tokens[4].replace('%', ''), 10); + callback(null, { + used_gb: usedGb.toFixed(2), + total_gb: totalGb.toFixed(2), + percent: percent + }); + } catch (error) { + callback(error, null); + } + }); + }, + memory: function (callback) { + cmdExec([Boot2Docker.command(), 'ssh', 'free -m'], function (err, out) { + if (err) { + callback(err); + return; + } + try { + var lines = out.split('\n'); + var dataline = _.find(lines, function (line) { + return line.indexOf('-/+ buffers') !== -1; + }); + var tokens = dataline.split(' '); + tokens = tokens.filter(function(token) { + return token !== ''; + }); + var usedGb = parseInt(tokens[2], 10) / 1000; + var freeGb = parseInt(tokens[3], 10) / 1000; + var totalGb = usedGb + freeGb; + var percent = Math.round(usedGb / totalGb * 100); + callback(null, { + used_gb: usedGb.toFixed(2), + total_gb: totalGb.toFixed(2), + free_gb: freeGb.toFixed(2), + percent: percent + }); + } catch (error) { + callback(error); + } + }); + }, + createScratchImage: function (callback) { + cmdExec([Boot2Docker.command(), 'ssh', 'tar cv --files-from /dev/null | docker import - scratch'], function (err, out) { + callback(err); + }); + }, + stats: function (callback) { + var self = this; + self.state(function (err, state) { + if (err) { + callback(err); + return; + } + if (state === 'poweroff') { + callback(null, {state: state}); + return; + } + self.memoryUsage(function (err, mem) { + if (err) { + callback(null, {state: state}); + return; + } + self.diskUsage(function (err, disk) { + if (err) { + callback(null, {state: state, memory: mem}); + return; + } + callback(null, { + state: state, + memory: mem, + disk: disk + }); + }); + }); + }); + }, + sshKeyExists: function () { + return fs.existsSync(path.join(homeDir(), '.ssh', 'id_boot2docker')); + }, + + // Todo: move me to setup + waitWhileStatus: function (status, callback) { + var current = status; + async.whilst(function () { + return current === status; + }, function (callback) { + Boot2Docker.status(function (err, vmStatus) { + if (err) { + callback(err); + } else { + current = vmStatus.trim(); + callback(); + } + }); + }, function (err) { + callback(err); + }); + } +}; + +module.exports = Boot2Docker; diff --git a/src/ContainerDetails.react.js b/src/ContainerDetails.react.js index 7f0fbc7941..d753ef7c05 100644 --- a/src/ContainerDetails.react.js +++ b/src/ContainerDetails.react.js @@ -9,8 +9,8 @@ var remote = require('remote'); var dialog = remote.require('dialog'); var ContainerStore = require('./ContainerStore'); var ContainerUtil = require('./ContainerUtil'); -var docker = require('./docker'); -var boot2docker = require('./boot2docker'); +var docker = require('./Docker'); +var boot2docker = require('./Boot2Docker'); var ProgressBar = require('react-bootstrap/ProgressBar'); var Popover = require('react-bootstrap/Popover'); diff --git a/src/ContainerDetailsSettings.react.js b/src/ContainerDetailsSettings.react.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/ContainerList.react.js b/src/ContainerList.react.js index 0d00fa0e40..2e65a6ddd5 100644 --- a/src/ContainerList.react.js +++ b/src/ContainerList.react.js @@ -8,7 +8,7 @@ var RetinaImage = require('react-retina-image'); var ModalTrigger = require('react-bootstrap/ModalTrigger'); var ContainerModal = require('./ContainerModal.react'); var Header = require('./Header.react'); -var docker = require('./docker'); +var docker = require('./Docker'); var ContainerList = React.createClass({ componentWillMount: function () { diff --git a/src/ContainerStore.js b/src/ContainerStore.js index 3e79c24728..3f304fad0a 100644 --- a/src/ContainerStore.js +++ b/src/ContainerStore.js @@ -1,14 +1,14 @@ +var $ = require('jquery'); +var _ = require('underscore'); var EventEmitter = require('events').EventEmitter; var async = require('async'); var path = require('path'); var assign = require('object-assign'); var Stream = require('stream'); var Convert = require('ansi-to-html'); -var docker = require('./docker'); -var registry = require('./registry'); +var docker = require('./Docker'); +var registry = require('./Registry'); var ContainerUtil = require('./ContainerUtil'); -var $ = require('jquery'); -var _ = require('underscore'); var convert = new Convert(); diff --git a/src/ContainerUtil.js b/src/ContainerUtil.js index 1b396b0298..86aebd701a 100644 --- a/src/ContainerUtil.js +++ b/src/ContainerUtil.js @@ -1,5 +1,5 @@ var _ = require('underscore'); -var docker = require('./docker'); +var docker = require('./Docker'); var ContainerUtil = { env: function (container) { diff --git a/src/Containers.react.js b/src/Containers.react.js index 0372475515..8c527d978d 100644 --- a/src/Containers.react.js +++ b/src/Containers.react.js @@ -1,3 +1,6 @@ +var async = require('async'); +var _ = require('underscore'); +var $ = require('jquery'); var React = require('react/addons'); var Router = require('react-router'); var RetinaImage = require('react-retina-image'); @@ -6,11 +9,7 @@ var ContainerModal = require('./ContainerModal.react'); var ContainerStore = require('./ContainerStore'); var ContainerList = require('./ContainerList.react'); var Header = require('./Header.react'); -var async = require('async'); -var _ = require('underscore'); -var docker = require('./docker'); -var $ = require('jquery'); - +var docker = require('./Docker'); var Containers = React.createClass({ mixins: [Router.Navigation, Router.State], getInitialState: function () { diff --git a/src/Docker.js b/src/Docker.js new file mode 100644 index 0000000000..ac9701546a --- /dev/null +++ b/src/Docker.js @@ -0,0 +1,28 @@ +var fs = require('fs'); +var path = require('path'); +var dockerode = require('dockerode'); + +var Docker = { + host: null, + _client: null, + setHost: function(host) { + this.host = host; + var certDir = path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.boot2docker/certs/boot2docker-vm'); + if (!fs.existsSync(certDir)) { + return; + } + this._client = new dockerode({ + protocol: 'https', + host: this.host, + port: 2376, + ca: fs.readFileSync(path.join(certDir, 'ca.pem')), + cert: fs.readFileSync(path.join(certDir, 'cert.pem')), + key: fs.readFileSync(path.join(certDir, 'key.pem')) + }); + }, + client: function () { + return this._client; + } +}; + +module.exports = Docker; diff --git a/src/Main.js b/src/Main.js new file mode 100644 index 0000000000..4c485a892b --- /dev/null +++ b/src/Main.js @@ -0,0 +1,44 @@ +var module = require('module'); +require.main.paths.splice(0, 0, process.env.NODE_PATH); + +var Bugsnag = require('bugsnag-js'); +var React = require('react'); +var Router = require('react-router'); +var RetinaImage = require('react-retina-image'); +var async = require('async'); +var docker = require('./docker'); +var router = require('./router'); +var boot2docker = require('./boot2docker'); +var ContainerStore = require('./ContainerStore'); +var Menu = require('./Menu'); +var remote = require('remote'); +var app = remote.require('app'); +var ipc = require('ipc'); + + +var Route = Router.Route; +var NotFoundRoute = Router.NotFoundRoute; +var DefaultRoute = Router.DefaultRoute; +var Link = Router.Link; +var RouteHandler = Router.RouteHandler; + +Bugsnag.apiKey = 'fc51aab02ce9dd1bb6ebc9fe2f4d43d7'; +Bugsnag.autoNotify = true; +Bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production'; +Bugsnag.notifyReleaseStages = []; +Bugsnag.appVersion = app.getVersion(); + +if (!window.location.hash.length || window.location.hash === '#/') { + router.run(function (Handler) { + React.render(, document.body); + }); +} else { + boot2docker.ip(function (err, ip) { + docker.setHost(ip); + ContainerStore.init(function () { + router.run(function (Handler) { + React.render(, document.body); + }); + }); + }); +} diff --git a/src/Menu.js b/src/Menu.js index 68bcd9cf2a..336209f99f 100644 --- a/src/Menu.js +++ b/src/Menu.js @@ -3,7 +3,7 @@ var app = remote.require('app'); var Menu = remote.require('menu'); var MenuItem = remote.require('menu-item'); var BrowserWindow = remote.require('browser-window'); -var router = require('./router'); +var router = require('./Router'); // main.js var template = [ diff --git a/src/Registry.js b/src/Registry.js new file mode 100644 index 0000000000..2fc0dcf0be --- /dev/null +++ b/src/Registry.js @@ -0,0 +1,84 @@ +var async = require('async'); +var $ = require('jquery'); + +var Registry = { + token: function(repository, callback) { + $.ajax({ + url: 'https://registry.hub.docker.com/v1/repositories/' + repository + '/images', + headers: { + 'X-Docker-Token': true, + }, + success: function (res, status, xhr) { + callback(null, xhr.getResponseHeader('X-Docker-Token')); + }, + error: function (err) { + callback(err); + } + }); + }, + ancestry: function (imageId, token, callback) { + $.ajax({ + url: 'https://registry-1.docker.io/v1/images/' + imageId + '/ancestry', + headers: { + Authorization: 'Token ' + token + }, + success: function (layers, status, xhr) { + callback(null, layers); + }, + error: function (err) { + callback(err); + } + }); + }, + imageId: function (repository, tag, token, callback) { + $.ajax({ + url: 'https://registry-1.docker.io/v1/repositories/' + repository + '/tags/' + tag, + headers: { + Authorization: 'Token ' + token + }, + success: function (res, status, xhr) { + callback(null, res); + }, + error: function (err) { + callback(err); + } + }); + }, + + // Returns an array [{Id: <12 character image ID, size: size of layer in bytes}] + layers: function (repository, tag, callback) { + var self = this; + this.token(repository, function (err, token) { + self.imageId(repository, tag, token, function (err, imageId) { + self.ancestry(imageId, token, function (err, layers) { + async.map(layers, function (layer, callback) { + $.ajax({ + url: 'https://registry-1.docker.io/v1/images/' + layer + '/json', + headers: { + Authorization: 'Token ' + token + }, + success: function (res, status, xhr) { + var size = xhr.getResponseHeader('X-Docker-Size'); + callback(null, { + Id: layer.slice(0, 12), + size: parseInt(size, 10) + }); + }, + error: function (err) { + callback(err); + } + }); + }, function (err, results) { + if (err) { + callback('Could not sum' + err); + return; + } + callback(null, results); + }); + }); + }); + }); + } +}; + +module.exports = Registry; diff --git a/src/Routes.js b/src/Routes.js new file mode 100644 index 0000000000..92b9df7d3a --- /dev/null +++ b/src/Routes.js @@ -0,0 +1,33 @@ +var React = require('react/addons'); +var Setup = require('./Setup.react'); +var Containers = require('./Containers.react'); +var ContainerDetails = require('./ContainerDetails.react'); +var Preferences = require('./Preferences.react'); +var NoContainers = require('./NoContainers.react'); +var Router = require('react-router'); + +var Route = Router.Route; +var DefaultRoute = Router.DefaultRoute; +var RouteHandler = Router.RouteHandler; + +var App = React.createClass({ + render: function () { + return ( + + ); + } +}); + +var routes = ( + + + + + + + + + +); + +module.exports = routes; diff --git a/src/Setup.react.js b/src/Setup.react.js index 5c07232533..5a77893b5a 100644 --- a/src/Setup.react.js +++ b/src/Setup.react.js @@ -5,11 +5,11 @@ var async = require('async'); var assign = require('object-assign'); var fs = require('fs'); var path = require('path'); -var boot2docker = require('./boot2docker.js'); -var virtualbox = require('./virtualbox.js'); -var util = require('./util.js'); -var docker = require('./docker.js'); -var ContainerStore = require('./ContainerStore.js'); +var boot2docker = require('./Boot2Docker'); +var virtualbox = require('./Virtualbox'); +var util = require('./Util'); +var docker = require('./Docker'); +var ContainerStore = require('./ContainerStore'); var setupSteps = [ { diff --git a/src/Util.js b/src/Util.js new file mode 100644 index 0000000000..78a4c61035 --- /dev/null +++ b/src/Util.js @@ -0,0 +1,103 @@ +var path = require('path'); +var fs = require('fs'); +var nodeCrypto = require('crypto'); +var request = require('request'); +var progress = require('request-progress'); +var exec = require('exec'); + +var Util = { + supportDir: function (callback) { + var dirs = ['Application\ Support', 'Kitematic']; + var acc = process.env.HOME; + dirs.forEach(function (d) { + acc = path.join(acc, d); + if (!fs.existsSync(acc)) { + fs.mkdirSync(acc); + } + }); + return acc; + }, + download: function (url, filename, checksum, callback, progressCallback) { + var doDownload = function () { + progress(request({ + uri: url, + rejectUnauthorized: false + }), { + throttle: 250 + }).on('progress', function (state) { + progressCallback(state.percent); + }).on('error', function (err) { + callback(err); + }).pipe(fs.createWriteStream(filename)).on('error', function (err) { + callback(err); + }).on('close', function (err) { + callback(err); + }); + }; + + // Compare checksum to see if it already exists first + if (fs.existsSync(filename)) { + var existingChecksum = nodeCrypto.createHash('sha256').update(fs.readFileSync(filename), 'utf8').digest('hex'); + console.log(existingChecksum); + if (existingChecksum !== checksum) { + fs.unlinkSync(filename); + doDownload(); + } else { + callback(); + } + } else { + doDownload(); + } + }, + 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; + } +}; + +module.exports = Util; diff --git a/src/router.js b/src/router.js index d419f62176..b02ac78139 100644 --- a/src/router.js +++ b/src/router.js @@ -1,5 +1,5 @@ var Router = require('react-router'); -var routes = require('./routes'); +var routes = require('./Routes'); var router = Router.create({ routes: routes diff --git a/src/virtualbox.js b/src/virtualbox.js index a1071e8e8f..503d4efad2 100644 --- a/src/virtualbox.js +++ b/src/virtualbox.js @@ -2,7 +2,7 @@ var fs = require('fs'); var exec = require('exec'); var path = require('path'); var async = require('async'); -var util = require('./util'); +var util = require('./Util'); var VirtualBox = { REQUIRED_VERSION: '4.3.18',