diff --git a/Gruntfile.js b/Gruntfile.js index ee2afeadc6..5b93ba7e1a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -22,6 +22,7 @@ module.exports = function (grunt) { var BASENAME = 'Kitematic'; var OSX_APPNAME = BASENAME + ' (Beta)'; var WINDOWS_APPNAME = BASENAME + ' (Alpha)'; + var LINUX_APPNAME = BASENAME + ' (Alpha)'; var OSX_OUT = './dist'; var OSX_OUT_X64 = OSX_OUT + '/' + OSX_APPNAME + '-darwin-x64'; var OSX_FILENAME = OSX_OUT_X64 + '/' + OSX_APPNAME + '.app'; @@ -57,6 +58,19 @@ module.exports = function (grunt) { 'app-bundle-id': 'com.kitematic.kitematic', 'app-version': packagejson.version } + }, + linux: { + options: { + name: LINUX_APPNAME, + dir: 'build/', + out: 'dist/linux/', + version: packagejson['electron-version'], + platform: 'linux', + arch: 'x64', + asar: true, + 'app-bundle-id': 'com.kitematic.kitematic', + 'app-version': packagejson.version + } } }, @@ -243,7 +257,11 @@ module.exports = function (grunt) { }); grunt.registerTask('default', ['newer:babel', 'less', 'newer:copy:dev', 'shell:electron', 'watchChokidar']); - grunt.registerTask('release', ['clean:release', 'babel', 'less', 'copy:dev', 'electron', 'copy:osx', 'shell:sign', 'shell:zip', 'copy:windows', 'rcedit:exes', 'compress']); + if(process.platform === 'linux') { + grunt.registerTask('release', ['clean:release', 'babel', 'less', 'copy:dev', 'electron:linux']); + } else { + grunt.registerTask('release', ['clean:release', 'babel', 'less', 'copy:dev', 'electron', 'copy:osx', 'shell:sign', 'shell:zip', 'copy:windows', 'rcedit:exes', 'compress']); + } process.on('SIGINT', function () { grunt.task.run(['shell:electron:kill']); diff --git a/README.md b/README.md index 6939a181d6..52c9f1ec20 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Kitematic Logo](https://cloud.githubusercontent.com/assets/251292/5269258/1b229c3c-7a2f-11e4-96f1-e7baf3c86d73.png)](https://kitematic.com) -Kitematic is a simple application for managing Docker containers on Mac and Windows. +Kitematic is a simple application for managing Docker containers on Mac, Linux and Windows. ![Kitematic Screenshot](https://cloud.githubusercontent.com/assets/251292/8246120/d3ab271a-15ed-11e5-8736-9a730a27c79a.png) diff --git a/src/app.js b/src/app.js index 45f46a4d54..360556f8e5 100644 --- a/src/app.js +++ b/src/app.js @@ -17,6 +17,7 @@ import Router from 'react-router'; import routes from './routes'; import routerContainer from './router'; import repositoryActions from './actions/RepositoryActions'; +import util from './utils/Util'; var app = remote.require('app'); hubUtil.init(); @@ -46,7 +47,8 @@ var router = Router.create({ router.run(Handler => React.render(, document.body)); routerContainer.set(router); -setupUtil.setup().then(() => { +let setup = util.isLinux() ? setupUtil.nativeSetup : setupUtil.nonNativeSetup; +setup().then(() => { Menu.setApplicationMenu(Menu.buildFromTemplate(template())); docker.init(); if (!hub.prompted() && !hub.loggedin()) { diff --git a/src/components/Preferences.react.js b/src/components/Preferences.react.js index 2326d11d7b..7884b72714 100644 --- a/src/components/Preferences.react.js +++ b/src/components/Preferences.react.js @@ -35,10 +35,11 @@ var Preferences = React.createClass({ }); }, render: function () { - return ( -
-
- Go Back + var vmSettings; + + if (process.platform !== 'linux') { + vmSettings = ( +
VM Settings
@@ -48,6 +49,15 @@ var Preferences = React.createClass({
+
+ ); + } + + return ( +
+
+ Go Back + {vmSettings}
App Settings
diff --git a/src/components/Setup.react.js b/src/components/Setup.react.js index eb44736a5b..741322e174 100644 --- a/src/components/Setup.react.js +++ b/src/components/Setup.react.js @@ -3,6 +3,7 @@ import Router from 'react-router'; import Radial from './Radial.react.js'; import RetinaImage from 'react-retina-image'; import Header from './Header.react'; +import Util from '../utils/Util'; import metrics from '../utils/MetricsUtil'; import setupStore from '../stores/SetupStore'; import setupActions from '../actions/SetupActions'; @@ -43,6 +44,13 @@ var Setup = React.createClass({ shell.openExternal('https://www.docker.com/docker-toolbox'); }, + handleLinuxDockerInstall: function () { + metrics.track('Opening Linux Docker installation instructions', { + from: 'setup' + }); + shell.openExternal('http://docs.docker.com/linux/started/'); + }, + renderContents: function () { return (
@@ -74,6 +82,25 @@ var Setup = React.createClass({ }, renderError: function () { + let deleteVmAndRetry; + + if (Util.isLinux()) { + if (!this.state.started) { + deleteVmAndRetry = ( + + ); + } + } else { + if (this.state.started) { + deleteVmAndRetry = ( + + ); + } else { + deleteVmAndRetry = ( + + ); + } + } return (
@@ -93,7 +120,7 @@ var Setup = React.createClass({

{this.state.error.message || this.state.error}

- {this.state.started ? : } + {{deleteVmAndRetry}}

diff --git a/src/utils/DockerMachineUtil.js b/src/utils/DockerMachineUtil.js index 283b4cee85..fa22e37eab 100644 --- a/src/utils/DockerMachineUtil.js +++ b/src/utils/DockerMachineUtil.js @@ -151,6 +151,11 @@ var DockerMachine = { } }); }); + } else if (util.isLinux()) { + cmd = cmd || process.env.SHELL; + var terminal = util.linuxTerminal(); + if (terminal) + util.exec(terminal.concat([cmd])).then(() => {}); } else { cmd = cmd || process.env.SHELL; this.url(machineName).then(machineUrl => { diff --git a/src/utils/DockerUtil.js b/src/utils/DockerUtil.js index 7091482feb..1eede0525e 100644 --- a/src/utils/DockerUtil.js +++ b/src/utils/DockerUtil.js @@ -3,6 +3,7 @@ import fs from 'fs'; import path from 'path'; import dockerode from 'dockerode'; import _ from 'underscore'; +import child_process from 'child_process'; import util from './Util'; import hubUtil from './HubUtil'; import metrics from '../utils/MetricsUtil'; @@ -20,20 +21,25 @@ export default { throw new Error('Falsy ip or name passed to docker client setup'); } - let certDir = path.join(util.home(), '.docker/machine/machines/', name); - if (!fs.existsSync(certDir)) { - throw new Error('Certificate directory does not exist'); - } + if (util.isLinux()) { + this.host = 'localhost'; + this.client = new dockerode({socketPath: '/var/run/docker.sock'}); + } else { + let certDir = path.join(util.home(), '.docker/machine/machines/', name); + if (!fs.existsSync(certDir)) { + throw new Error('Certificate directory does not exist'); + } - this.host = ip; - this.client = new dockerode({ - protocol: 'https', - host: ip, - 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')) - }); + this.host = ip; + this.client = new dockerode({ + protocol: 'https', + host: ip, + 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')) + }); + } }, init () { @@ -66,6 +72,14 @@ export default { }); }, + isDockerRunning () { + try { + child_process.execSync('ps ax | grep "docker daemon" | grep -v grep'); + } catch (error) { + throw new Error('Cannot connect to the Docker daemon. The daemon is not running.'); + } + }, + startContainer (name, containerData) { let startopts = { Binds: containerData.Binds || [] diff --git a/src/utils/SetupUtil.js b/src/utils/SetupUtil.js index b0cf1ba0d6..cf9b6ac43f 100644 --- a/src/utils/SetupUtil.js +++ b/src/utils/SetupUtil.js @@ -2,8 +2,8 @@ import _ from 'underscore'; import fs from 'fs'; import path from 'path'; import Promise from 'bluebird'; -import util from './Util'; import bugsnag from 'bugsnag-js'; +import util from './Util'; import virtualBox from './VirtualBoxUtil'; import setupServerActions from '../actions/SetupServerActions'; import metrics from './MetricsUtil'; @@ -51,7 +51,31 @@ export default { return _retryPromise.promise; }, - async setup () { + async nativeSetup () { + while (true) { + try { + docker.setup('localhost', machine.name()); + docker.isDockerRunning(); + + break; + } catch (error) { + router.get().transitionTo('setup'); + metrics.track('Native Setup Failed'); + setupServerActions.error({error}); + + let message = error.message.split('\n'); + let lastLine = message.length > 1 ? message[message.length - 2] : 'Docker Machine encountered an error.'; + bugsnag.notify('Native Setup Failed', lastLine, { + 'Docker Machine Logs': error.message + }, 'info'); + + this.clearTimers(); + await this.pause(); + } + } + }, + + async nonNativeSetup () { let virtualBoxVersion = null; let machineVersion = null; while (true) { diff --git a/src/utils/Util.js b/src/utils/Util.js index 9950ca6ec1..a0671a57ad 100644 --- a/src/utils/Util.js +++ b/src/utils/Util.js @@ -5,6 +5,7 @@ import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import remote from 'remote'; +var dialog = remote.require('dialog'); var app = remote.require('app'); module.exports = { @@ -34,6 +35,9 @@ module.exports = { isWindows: function () { return process.platform === 'win32'; }, + isLinux: function () { + return process.platform === 'linux'; + }, binsPath: function () { return this.isWindows() ? path.join(this.home(), 'Kitematic-bins') : path.join('/usr/local/bin'); }, @@ -156,5 +160,17 @@ module.exports = { linuxToWindowsPath: function (linuxAbsPath) { return linuxAbsPath.replace('/c', 'C:').split('/').join('\\'); }, + linuxTerminal: function () { + if (fs.existsSync('/usr/bin/x-terminal-emulator')) { + return ['/usr/bin/x-terminal-emulator', '-e']; + } else { + dialog.showMessageBox({ + type: 'warning', + buttons: ['OK'], + message: 'The terminal emulator symbolic link doesn\'t exists. Please read the Wiki at https://github.com/kitematic/kitematic/wiki/Common-Issues-and-Fixes#early-linux-support-from-zedtux.' + }); + return; + } + }, webPorts: ['80', '8000', '8080', '8888', '3000', '5000', '2368', '9200', '8983'] }; diff --git a/styles/variables.less b/styles/variables.less index a98c2fe98d..9e873d583d 100644 --- a/styles/variables.less +++ b/styles/variables.less @@ -23,8 +23,8 @@ @color-box-button: lighten(@gray-lightest, 5%); @color-background: lighten(@gray-lightest, 4.5%); -@font-regular: "Helvetica Neue", Segoe UI, Arial, "Lucida Grande", sans-serif; -@font-code: Menlo, Consolas; +@font-regular: "Helvetica Neue", Segoe UI, "Ubuntu", Arial, "Lucida Grande", sans-serif; +@font-code: Menlo, Consolas, "DejaVu Sans Mono"; @border-radius: 0.2rem;