Implement Linux support

This commit is contained in:
Guillaume Hain 2015-06-20 12:28:00 +02:00
parent 0ed907553b
commit de0b2efd2a
No known key found for this signature in database
GPG Key ID: D2B2A8B99474CF36
10 changed files with 141 additions and 25 deletions

View File

@ -22,6 +22,7 @@ module.exports = function (grunt) {
var BASENAME = 'Kitematic'; var BASENAME = 'Kitematic';
var OSX_APPNAME = BASENAME + ' (Beta)'; var OSX_APPNAME = BASENAME + ' (Beta)';
var WINDOWS_APPNAME = BASENAME + ' (Alpha)'; var WINDOWS_APPNAME = BASENAME + ' (Alpha)';
var LINUX_APPNAME = BASENAME + ' (Alpha)';
var OSX_OUT = './dist'; var OSX_OUT = './dist';
var OSX_OUT_X64 = OSX_OUT + '/' + OSX_APPNAME + '-darwin-x64'; var OSX_OUT_X64 = OSX_OUT + '/' + OSX_APPNAME + '-darwin-x64';
var OSX_FILENAME = OSX_OUT_X64 + '/' + OSX_APPNAME + '.app'; var OSX_FILENAME = OSX_OUT_X64 + '/' + OSX_APPNAME + '.app';
@ -57,6 +58,19 @@ module.exports = function (grunt) {
'app-bundle-id': 'com.kitematic.kitematic', 'app-bundle-id': 'com.kitematic.kitematic',
'app-version': packagejson.version '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('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 () { process.on('SIGINT', function () {
grunt.task.run(['shell:electron:kill']); grunt.task.run(['shell:electron:kill']);

View File

@ -3,7 +3,7 @@
[![Kitematic Logo](https://cloud.githubusercontent.com/assets/251292/5269258/1b229c3c-7a2f-11e4-96f1-e7baf3c86d73.png)](https://kitematic.com) [![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) ![Kitematic Screenshot](https://cloud.githubusercontent.com/assets/251292/8246120/d3ab271a-15ed-11e5-8736-9a730a27c79a.png)

View File

@ -17,6 +17,7 @@ import Router from 'react-router';
import routes from './routes'; import routes from './routes';
import routerContainer from './router'; import routerContainer from './router';
import repositoryActions from './actions/RepositoryActions'; import repositoryActions from './actions/RepositoryActions';
import util from './utils/Util';
var app = remote.require('app'); var app = remote.require('app');
hubUtil.init(); hubUtil.init();
@ -46,7 +47,8 @@ var router = Router.create({
router.run(Handler => React.render(<Handler/>, document.body)); router.run(Handler => React.render(<Handler/>, document.body));
routerContainer.set(router); routerContainer.set(router);
setupUtil.setup().then(() => { let setup = util.isLinux() ? setupUtil.nativeSetup : setupUtil.nonNativeSetup;
setup().then(() => {
Menu.setApplicationMenu(Menu.buildFromTemplate(template())); Menu.setApplicationMenu(Menu.buildFromTemplate(template()));
docker.init(); docker.init();
if (!hub.prompted() && !hub.loggedin()) { if (!hub.prompted() && !hub.loggedin()) {

View File

@ -35,10 +35,11 @@ var Preferences = React.createClass({
}); });
}, },
render: function () { render: function () {
return ( var vmSettings;
<div className="preferences">
<div className="preferences-content"> if (process.platform !== 'linux') {
<a onClick={this.handleGoBackClick}>Go Back</a> vmSettings = (
<div>
<div className="title">VM Settings</div> <div className="title">VM Settings</div>
<div className="option"> <div className="option">
<div className="option-name"> <div className="option-name">
@ -48,6 +49,15 @@ var Preferences = React.createClass({
<input type="checkbox" checked={this.state.closeVMOnQuit} onChange={this.handleChangeCloseVMOnQuit}/> <input type="checkbox" checked={this.state.closeVMOnQuit} onChange={this.handleChangeCloseVMOnQuit}/>
</div> </div>
</div> </div>
</div>
);
}
return (
<div className="preferences">
<div className="preferences-content">
<a onClick={this.handleGoBackClick}>Go Back</a>
{vmSettings}
<div className="title">App Settings</div> <div className="title">App Settings</div>
<div className="option"> <div className="option">
<div className="option-name"> <div className="option-name">

View File

@ -3,6 +3,7 @@ import Router from 'react-router';
import Radial from './Radial.react.js'; import Radial from './Radial.react.js';
import RetinaImage from 'react-retina-image'; import RetinaImage from 'react-retina-image';
import Header from './Header.react'; import Header from './Header.react';
import Util from '../utils/Util';
import metrics from '../utils/MetricsUtil'; import metrics from '../utils/MetricsUtil';
import setupStore from '../stores/SetupStore'; import setupStore from '../stores/SetupStore';
import setupActions from '../actions/SetupActions'; import setupActions from '../actions/SetupActions';
@ -43,6 +44,13 @@ var Setup = React.createClass({
shell.openExternal('https://www.docker.com/docker-toolbox'); 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 () { renderContents: function () {
return ( return (
<div className="contents"> <div className="contents">
@ -74,6 +82,25 @@ var Setup = React.createClass({
}, },
renderError: function () { renderError: function () {
let deleteVmAndRetry;
if (Util.isLinux()) {
if (!this.state.started) {
deleteVmAndRetry = (
<button className="btn btn-action" onClick={this.handleLinuxDockerInstall}>Install Docker</button>
);
}
} else {
if (this.state.started) {
deleteVmAndRetry = (
<button className="btn btn-action" onClick={this.handleErrorRemoveRetry}>Delete VM &amp; Retry Setup</button>
);
} else {
deleteVmAndRetry = (
<button className="btn btn-action" onClick={this.handleToolBox}>Get Toolbox</button>
);
}
}
return ( return (
<div className="setup"> <div className="setup">
<Header hideLogin={true}/> <Header hideLogin={true}/>
@ -93,7 +120,7 @@ var Setup = React.createClass({
<p className="error">{this.state.error.message || this.state.error}</p> <p className="error">{this.state.error.message || this.state.error}</p>
<p className="setup-actions"> <p className="setup-actions">
<button className="btn btn-action" onClick={this.handleErrorRetry}>Retry Setup</button> <button className="btn btn-action" onClick={this.handleErrorRetry}>Retry Setup</button>
{this.state.started ? <button className="btn btn-action" onClick={this.handleErrorRemoveRetry}>Delete VM &amp; Retry Setup</button> : <button className="btn btn-action" onClick={this.handleToolBox}>Get Toolbox</button>} {{deleteVmAndRetry}}
</p> </p>
</div> </div>
</div> </div>

View File

@ -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 { } else {
cmd = cmd || process.env.SHELL; cmd = cmd || process.env.SHELL;
this.url(machineName).then(machineUrl => { this.url(machineName).then(machineUrl => {

View File

@ -3,6 +3,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import dockerode from 'dockerode'; import dockerode from 'dockerode';
import _ from 'underscore'; import _ from 'underscore';
import child_process from 'child_process';
import util from './Util'; import util from './Util';
import hubUtil from './HubUtil'; import hubUtil from './HubUtil';
import metrics from '../utils/MetricsUtil'; import metrics from '../utils/MetricsUtil';
@ -20,20 +21,25 @@ export default {
throw new Error('Falsy ip or name passed to docker client setup'); throw new Error('Falsy ip or name passed to docker client setup');
} }
let certDir = path.join(util.home(), '.docker/machine/machines/', name); if (util.isLinux()) {
if (!fs.existsSync(certDir)) { this.host = 'localhost';
throw new Error('Certificate directory does not exist'); 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.host = ip;
this.client = new dockerode({ this.client = new dockerode({
protocol: 'https', protocol: 'https',
host: ip, host: ip,
port: 2376, port: 2376,
ca: fs.readFileSync(path.join(certDir, 'ca.pem')), ca: fs.readFileSync(path.join(certDir, 'ca.pem')),
cert: fs.readFileSync(path.join(certDir, 'cert.pem')), cert: fs.readFileSync(path.join(certDir, 'cert.pem')),
key: fs.readFileSync(path.join(certDir, 'key.pem')) key: fs.readFileSync(path.join(certDir, 'key.pem'))
}); });
}
}, },
init () { 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) { startContainer (name, containerData) {
let startopts = { let startopts = {
Binds: containerData.Binds || [] Binds: containerData.Binds || []

View File

@ -2,8 +2,8 @@ import _ from 'underscore';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import Promise from 'bluebird'; import Promise from 'bluebird';
import util from './Util';
import bugsnag from 'bugsnag-js'; import bugsnag from 'bugsnag-js';
import util from './Util';
import virtualBox from './VirtualBoxUtil'; import virtualBox from './VirtualBoxUtil';
import setupServerActions from '../actions/SetupServerActions'; import setupServerActions from '../actions/SetupServerActions';
import metrics from './MetricsUtil'; import metrics from './MetricsUtil';
@ -51,7 +51,31 @@ export default {
return _retryPromise.promise; 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 virtualBoxVersion = null;
let machineVersion = null; let machineVersion = null;
while (true) { while (true) {

View File

@ -5,6 +5,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import crypto from 'crypto'; import crypto from 'crypto';
import remote from 'remote'; import remote from 'remote';
var dialog = remote.require('dialog');
var app = remote.require('app'); var app = remote.require('app');
module.exports = { module.exports = {
@ -34,6 +35,9 @@ module.exports = {
isWindows: function () { isWindows: function () {
return process.platform === 'win32'; return process.platform === 'win32';
}, },
isLinux: function () {
return process.platform === 'linux';
},
binsPath: function () { binsPath: function () {
return this.isWindows() ? path.join(this.home(), 'Kitematic-bins') : path.join('/usr/local/bin'); return this.isWindows() ? path.join(this.home(), 'Kitematic-bins') : path.join('/usr/local/bin');
}, },
@ -156,5 +160,17 @@ module.exports = {
linuxToWindowsPath: function (linuxAbsPath) { linuxToWindowsPath: function (linuxAbsPath) {
return linuxAbsPath.replace('/c', 'C:').split('/').join('\\'); 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'] webPorts: ['80', '8000', '8080', '8888', '3000', '5000', '2368', '9200', '8983']
}; };

View File

@ -23,8 +23,8 @@
@color-box-button: lighten(@gray-lightest, 5%); @color-box-button: lighten(@gray-lightest, 5%);
@color-background: lighten(@gray-lightest, 4.5%); @color-background: lighten(@gray-lightest, 4.5%);
@font-regular: "Helvetica Neue", Segoe UI, Arial, "Lucida Grande", sans-serif; @font-regular: "Helvetica Neue", Segoe UI, "Ubuntu", Arial, "Lucida Grande", sans-serif;
@font-code: Menlo, Consolas; @font-code: Menlo, Consolas, "DejaVu Sans Mono";
@border-radius: 0.2rem; @border-radius: 0.2rem;