From c02a03719008675b4196e1d1e56a5f5d9d11cf68 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Thu, 16 Apr 2015 14:54:40 -0400 Subject: [PATCH 1/9] Revert "Revert "Adding Custom URL Handler for running containers" for 1.6 release" --- .jshintrc | 2 +- src/ContainerHomeFolders.react.js | 34 ++++++++----- src/ContainerList.react.js | 8 +--- src/ContainerStore.js | 20 +++++++- src/Containers.react.js | 48 +++++-------------- src/Main.js | 46 ++++++++++++++---- src/NewContainerPull.react.js | 42 ++++++++++++++++ ...r.react.js => NewContainerSearch.react.js} | 6 +-- src/Routes.js | 24 ++++++---- src/browser.js | 19 ++++++++ styles/new-container.less | 29 +++++++++++ styles/theme.less | 1 - util/Info.plist | 11 +++++ 13 files changed, 210 insertions(+), 80 deletions(-) create mode 100644 src/NewContainerPull.react.js rename src/{NewContainer.react.js => NewContainerSearch.react.js} (95%) diff --git a/.jshintrc b/.jshintrc index daeaf45072..9240a3ae9f 100644 --- a/.jshintrc +++ b/.jshintrc @@ -27,5 +27,5 @@ "jest": true, "pit": true }, - "predef": [ "Promise" ] + "predef": [ "-Promise" ] } diff --git a/src/ContainerHomeFolders.react.js b/src/ContainerHomeFolders.react.js index 1ace6d4e90..2e3e796cc3 100644 --- a/src/ContainerHomeFolders.react.js +++ b/src/ContainerHomeFolders.react.js @@ -7,6 +7,7 @@ var util = require('./Util'); var metrics = require('./Metrics'); var Router = require('react-router'); var ContainerStore = require('./ContainerStore'); +var dialog = require('remote').require('dialog'); var ContainerHomeFolder = React.createClass({ mixins: [Router.State, Router.Navigation], @@ -16,20 +17,27 @@ var ContainerHomeFolder = React.createClass({ }); if (hostVolume.indexOf(process.env.HOME) === -1) { - var volumes = _.clone(this.props.container.Volumes); - var newHostVolume = path.join(util.home(), 'Kitematic', this.props.container.Name, containerVolume); - volumes[containerVolume] = newHostVolume; - var binds = _.pairs(volumes).map(function (pair) { - return pair[1] + ':' + pair[0]; - }); - ContainerStore.updateContainer(this.props.container.Name, { - Binds: binds - }, function (err) { - if (err) { - console.log(err); - return; + dialog.showMessageBox({ + message: 'Enable all volumes to edit files via Finder? This may not work with all database containers.', + buttons: ['Enable Volumes', 'Cancel'] + }, (index) => { + if (index === 0) { + var volumes = _.clone(this.props.container.Volumes); + var newHostVolume = path.join(util.home(), 'Kitematic', this.props.container.Name, containerVolume); + volumes[containerVolume] = newHostVolume; + var binds = _.pairs(volumes).map(function (pair) { + return pair[1] + ':' + pair[0]; + }); + ContainerStore.updateContainer(this.props.container.Name, { + Binds: binds + }, (err) => { + if (err) { + console.log(err); + return; + } + shell.showItemInFolder(newHostVolume); + }); } - shell.showItemInFolder(newHostVolume); }); } else { shell.showItemInFolder(hostVolume); diff --git a/src/ContainerList.react.js b/src/ContainerList.react.js index d2954dc603..1e6f358d6d 100644 --- a/src/ContainerList.react.js +++ b/src/ContainerList.react.js @@ -19,15 +19,9 @@ var ContainerList = React.createClass({ ); }); - var newItem; - if (!this.props.downloading) { - newItem = ; - } else { - newItem = ''; - } return (
    - {newItem} + {containers}
); diff --git a/src/ContainerStore.js b/src/ContainerStore.js index 109a022d19..262ddc6a57 100644 --- a/src/ContainerStore.js +++ b/src/ContainerStore.js @@ -14,6 +14,7 @@ var _progress = {}; var _muted = {}; var _blocked = {}; var _error = null; +var _pending = null; var ContainerStore = assign(Object.create(EventEmitter.prototype), { CLIENT_CONTAINER_EVENT: 'client_container_event', @@ -201,6 +202,10 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { var data = JSON.parse(json); console.log(data); + if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete') { + return; + } + // If the event is delete, remove the container if (data.status === 'destroy') { var container = _.findWhere(_.values(_containers), {Id: data.id}); @@ -226,7 +231,6 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { } }, init: function (callback) { - // TODO: Load cached data from db on loading this.fetchAllContainers(err => { if (err) { _error = err; @@ -471,6 +475,20 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { }, downloading: function () { return !!_.keys(_placeholders).length; + }, + pending: function () { + return _pending; + }, + setPending: function (repository, tag) { + _pending = { + repository: repository, + tag: tag + }; + this.emit(this.CLIENT_CONTAINER_EVENT, null, 'pending'); + }, + clearPending: function () { + _pending = null; + this.emit(this.CLIENT_CONTAINER_EVENT, null, 'pending'); } }); diff --git a/src/Containers.react.js b/src/Containers.react.js index af151dabca..ab613bb61a 100644 --- a/src/Containers.react.js +++ b/src/Containers.react.js @@ -10,8 +10,6 @@ var metrics = require('./Metrics'); var autoUpdater = remote.require('auto-updater'); var RetinaImage = require('react-retina-image'); var machine = require('./DockerMachine'); -var OverlayTrigger = require('react-bootstrap').OverlayTrigger; -var Tooltip = require('react-bootstrap').Tooltip; var util = require('./Util'); var Containers = React.createClass({ @@ -33,10 +31,6 @@ var Containers = React.createClass({ ContainerStore.on(ContainerStore.SERVER_CONTAINER_EVENT, this.update); ContainerStore.on(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient); - if (this.state.sorted.length) { - this.transitionTo('containerHome', {name: this.state.sorted[0].Name}); - } - ipc.on('application:update-available', () => { this.setState({ updateAvailable: true @@ -48,38 +42,33 @@ var Containers = React.createClass({ ContainerStore.removeListener(ContainerStore.SERVER_CONTAINER_EVENT, this.update); ContainerStore.removeListener(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient); }, - onDestroy: function () { - if (this.state.sorted.length) { - this.transitionTo('containerHome', {name: this.state.sorted[0].Name}); - } else { - this.transitionTo('containers'); - } - }, updateError: function (err) { this.setState({ error: err }); }, update: function (name, status) { + var sorted = ContainerStore.sorted(); this.setState({ containers: ContainerStore.containers(), - sorted: ContainerStore.sorted(), + sorted: sorted, + pending: ContainerStore.pending(), downloading: ContainerStore.downloading() }); if (status === 'destroy') { - this.onDestroy(); + if (sorted.length) { + this.transitionTo('containerHome', {name: sorted[0].Name}); + } else { + this.transitionTo('containers'); + } } }, updateFromClient: function (name, status) { - this.setState({ - containers: ContainerStore.containers(), - sorted: ContainerStore.sorted(), - downloading: ContainerStore.downloading() - }); + this.update(name, status); if (status === 'create') { this.transitionTo('containerHome', {name: name}); - } else if (status === 'destroy') { - this.onDestroy(); + } else if (status === 'pending' && ContainerStore.pending()) { + this.transitionTo('pull'); } }, handleScroll: function (e) { @@ -162,17 +151,6 @@ var Containers = React.createClass({ ); } - var button; - if (this.state.downloading) { - button = ( - Only one Docker image can be downloaded at a time.}> - - - ); - } else { - button = ; - } - var container = this.getParams().name ? this.state.containers[this.getParams().name] : {}; return (
@@ -182,7 +160,7 @@ var Containers = React.createClass({

Containers

- {button} +
@@ -197,7 +175,7 @@ var Containers = React.createClass({
- + ); diff --git a/src/Main.js b/src/Main.js index 9f582f3588..44235be0bc 100644 --- a/src/Main.js +++ b/src/Main.js @@ -27,14 +27,48 @@ setInterval(function () { router.run(Handler => React.render(, document.body)); +ipc.on('application:quitting', opts => { + if (!opts.updating && localStorage.getItem('settings.closeVMOnQuit') === 'true') { + machine.stop(); + } +}); + +ipc.on('application:open-url', opts => { + var repoRegexp = /[a-z0-9]+(?:[._-][a-z0-9]+)*/; + var parser = document.createElement('a'); + parser.href = opts.url; + + if (parser.protocol !== 'docker:') { + return; + } + + var pathname = parser.pathname.replace('//', ''); + var tokens = pathname.split('/'); + var type = tokens[0]; + var method = tokens[1]; + var repo = tokens.slice(2).join('/'); + + // Only accept official repos for now + if (repo.indexOf('/') !== -1 || !repoRegexp.test(repo)) { + return; + } + + if (type === 'repository' && method === 'run') { + ContainerStore.setPending(repo, 'latest'); + } +}); + SetupStore.setup().then(() => { + if (ContainerStore.pending()) { + router.transitionTo('pull'); + } else { + router.transitionTo('new'); + } Menu.setApplicationMenu(Menu.buildFromTemplate(template())); ContainerStore.on(ContainerStore.SERVER_ERROR_EVENT, (err) => { bugsnag.notify(err); }); - ContainerStore.init(function () { - router.transitionTo('containers'); - }); + ContainerStore.init(function () {}); }).catch(err => { metrics.track('Setup Failed', { step: 'catch', @@ -43,9 +77,3 @@ SetupStore.setup().then(() => { console.log(err); bugsnag.notify(err); }); - -ipc.on('application:quitting', opts => { - if (!opts.updating && localStorage.getItem('settings.closeVMOnQuit') === 'true') { - machine.stop(); - } -}); diff --git a/src/NewContainerPull.react.js b/src/NewContainerPull.react.js new file mode 100644 index 0000000000..bc915b4dda --- /dev/null +++ b/src/NewContainerPull.react.js @@ -0,0 +1,42 @@ +var React = require('react/addons'); +var Router = require('react-router'); +var shell = require('shell'); +var ContainerStore = require('./ContainerStore'); + +module.exports = React.createClass({ + mixins: [Router.Navigation], + handleOpenClick: function () { + var repo = this.props.pending.repository; + if (repo.indexOf('/') === -1) { + shell.openExternal(`https://registry.hub.docker.com/_/${this.props.pending.repository}`); + } else { + shell.openExternal(`https://registry.hub.docker.com/u/${this.props.pending.repository}`); + } + }, + handleCancelClick: function () { + ContainerStore.clearPending(); + this.transitionTo('new'); + }, + handleConfirmClick: function () { + ContainerStore.clearPending(); + ContainerStore.create(this.props.pending.repository, this.props.pending.tag, function () {}); + }, + render: function () { + if (!this.props.pending) { + return false; + } + return ( +
+
+
+

You're about to download and run {this.props.pending.repository}:{this.props.pending.tag}.

+

Please confirm to create the container.

+ +
+
+
+ ); + } +}); diff --git a/src/NewContainer.react.js b/src/NewContainerSearch.react.js similarity index 95% rename from src/NewContainer.react.js rename to src/NewContainerSearch.react.js index 05e772cedd..3ac67f2882 100644 --- a/src/NewContainer.react.js +++ b/src/NewContainerSearch.react.js @@ -10,7 +10,7 @@ var metrics = require('./Metrics'); var _recommended = []; var _searchPromise = null; -var NewContainer = React.createClass({ +module.exports = React.createClass({ getInitialState: function () { return { query: '', @@ -47,7 +47,7 @@ var NewContainer = React.createClass({ loading: true }); - _searchPromise = Promise.delay(200).then(() => Promise.resolve($.get('https://registry.hub.docker.com/v1/search?q=' + query))).cancellable().then(data => { + _searchPromise = Promise.delay(200).cancellable().then(() => Promise.resolve($.get('https://registry.hub.docker.com/v1/search?q=' + query))).then(data => { metrics.track('Searched for Images'); this.setState({ results: data.results, @@ -166,5 +166,3 @@ var NewContainer = React.createClass({ ); } }); - -module.exports = NewContainer; diff --git a/src/Routes.js b/src/Routes.js index eab468fa1c..cfdade8b51 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -9,12 +9,14 @@ var ContainerSettingsGeneral = require('./ContainerSettingsGeneral.react'); var ContainerSettingsPorts = require('./ContainerSettingsPorts.react'); var ContainerSettingsVolumes = require('./ContainerSettingsVolumes.react'); var Preferences = require('./Preferences.react'); -var NewContainer = require('./NewContainer.react'); +var NewContainerSearch = require('./NewContainerSearch.react'); +var NewContainerPull = require('./NewContainerPull.react'); var Router = require('react-router'); var Route = Router.Route; var DefaultRoute = Router.DefaultRoute; var RouteHandler = Router.RouteHandler; +var Redirect = Router.Redirect; var App = React.createClass({ render: function () { @@ -27,17 +29,21 @@ var App = React.createClass({ var routes = ( - - - - - - - + + + + + + + + + + + - + diff --git a/src/browser.js b/src/browser.js index 8722673dcb..ee7810450a 100644 --- a/src/browser.js +++ b/src/browser.js @@ -18,6 +18,13 @@ try { settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); } catch (err) {} + +var openURL = null; +app.on('open-url', function (event, url) { + event.preventDefault(); + openURL = url; +}); + app.on('ready', function () { var mainWindow = new BrowserWindow({ width: size.width || 1000, @@ -65,6 +72,18 @@ app.on('ready', function () { mainWindow.show(); mainWindow.focus(); + if (openURL) { + mainWindow.webContents.send('application:open-url', { + url: openURL + }); + } + app.on('open-url', function (event, url) { + event.preventDefault(); + mainWindow.webContents.send('application:open-url', { + url: url + }); + }); + if (process.env.NODE_ENV !== 'development') { autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion() + '&beta=' + !!settingsjson.beta); } diff --git a/styles/new-container.less b/styles/new-container.less index a00f9c3eb3..86b60d8903 100644 --- a/styles/new-container.less +++ b/styles/new-container.less @@ -1,3 +1,32 @@ +.new-container-pull { + display: flex; + flex: 1 auto; + align-items: center; + justify-content: center; + .content { + text-align: center; + + .buttons { + margin-top: 30px; + .btn { + margin-left: 10px; + margin-right: 10px; + padding: 8px 18px; + font-size: 14px; + background: white; + font-weight: 300; + } + } + } + h1 { + font-size: 20px; + color: @gray-normal; + font-weight: 400; + text-align: center; + margin-top: 10px; + } +} + .new-container { display: flex; flex: 1 auto; diff --git a/styles/theme.less b/styles/theme.less index 07e8cedd22..d20daccc76 100644 --- a/styles/theme.less +++ b/styles/theme.less @@ -134,7 +134,6 @@ input[type="text"] { font-weight: 400; text-shadow: none; padding: 5px 14px 5px 14px; - height: 30px; cursor: default; &.small { diff --git a/util/Info.plist b/util/Info.plist index 396be69183..5efd29e633 100644 --- a/util/Info.plist +++ b/util/Info.plist @@ -26,5 +26,16 @@ AtomApplication NSSupportsAutomaticGraphicsSwitching + CFBundleURLTypes + + + CFBundleURLSchemes + + docker + + CFBundleURLName + Docker App Protocol + + From 1fa8127145863310ec06311017acca13689bf433 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Thu, 16 Apr 2015 15:24:30 -0400 Subject: [PATCH 2/9] Adding unit test for official repo detection --- src/Main.js | 4 ++-- src/Util-test.js | 24 ++++++++++++++++++++++++ src/Util.js | 7 +++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/Util-test.js diff --git a/src/Main.js b/src/Main.js index 44235be0bc..1385e2791b 100644 --- a/src/Main.js +++ b/src/Main.js @@ -11,6 +11,7 @@ var metrics = require('./Metrics'); var router = require('./Router'); var template = require('./MenuTemplate'); var webUtil = require('./WebUtil'); +var util = require ('./Util'); webUtil.addWindowSizeSaving(); webUtil.addLiveReload(); @@ -34,7 +35,6 @@ ipc.on('application:quitting', opts => { }); ipc.on('application:open-url', opts => { - var repoRegexp = /[a-z0-9]+(?:[._-][a-z0-9]+)*/; var parser = document.createElement('a'); parser.href = opts.url; @@ -49,7 +49,7 @@ ipc.on('application:open-url', opts => { var repo = tokens.slice(2).join('/'); // Only accept official repos for now - if (repo.indexOf('/') !== -1 || !repoRegexp.test(repo)) { + if (repo.indexOf('/') !== -1 || !util.isOfficialRepo(repo)) { return; } diff --git a/src/Util-test.js b/src/Util-test.js new file mode 100644 index 0000000000..f4a0582195 --- /dev/null +++ b/src/Util-test.js @@ -0,0 +1,24 @@ +jest.dontMock('./Util'); +var util = require('./Util'); + +describe('Util', function () { + it('accepts official repo', () => { + expect(util.isOfficialRepo('redis')).toBe(true); + }); + + it('rejects falsy value as official repo', () => { + expect(util.isOfficialRepo(undefined)).toBe(false); + }); + + it('rejects empty repo name', () => { + expect(util.isOfficialRepo('')).toBe(false); + }); + + it('rejects repo with non official namespace', () => { + expect(util.isOfficialRepo('kitematic/html')).toBe(false); + }); + + it('rejects repo with a different registry address', () => { + expect(util.isOfficialRepo('www.myregistry.com/kitematic/html')).toBe(false); + }); +}); diff --git a/src/Util.js b/src/Util.js index a9ab1b2e0d..54df0d2fcf 100644 --- a/src/Util.js +++ b/src/Util.js @@ -44,5 +44,12 @@ module.exports = { } catch (err) {} return settingsjson; }, + isOfficialRepo: function (name) { + if (!name || !name.length) { + return false; + } + var repoRegexp = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/; + return repoRegexp.test(name); + }, webPorts: ['80', '8000', '8080', '3000', '5000', '2368', '9200', '8983'] }; From 2a7d8230d334b28c6cc3b2761acfa6a3ecc47739 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Thu, 16 Apr 2015 15:27:14 -0400 Subject: [PATCH 3/9] Removing unnecessary check --- src/Main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Main.js b/src/Main.js index 1385e2791b..8b4d4fbda1 100644 --- a/src/Main.js +++ b/src/Main.js @@ -49,7 +49,7 @@ ipc.on('application:open-url', opts => { var repo = tokens.slice(2).join('/'); // Only accept official repos for now - if (repo.indexOf('/') !== -1 || !util.isOfficialRepo(repo)) { + if (!util.isOfficialRepo(repo)) { return; } From 3869b5ca18567f73e6a3b36c8817c82385d4a769 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Mon, 27 Apr 2015 11:30:23 -0400 Subject: [PATCH 4/9] Unit tests for url opener --- package.json | 1 + src/app.js | 41 +++++++++------------------ src/stores/SetupStore-test.js | 6 ++-- src/stores/SetupStore.js | 6 ++-- src/utils/SetupUtil.js | 49 -------------------------------- src/utils/URLUtil-test.js | 53 +++++++++++++++++++++++++++++++++++ src/utils/URLUtil.js | 44 +++++++++++++++++++++++++++++ src/utils/Util.js | 49 ++++++++++++++++++++++++++++++++ 8 files changed, 166 insertions(+), 83 deletions(-) create mode 100644 src/utils/URLUtil-test.js create mode 100644 src/utils/URLUtil.js diff --git a/package.json b/package.json index dbf5d3f6c9..35ae89fd23 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "mixpanel": "0.2.0", "node-uuid": "^1.4.3", "object-assign": "^2.0.0", + "parseUri": "^1.2.3-2", "react": "^0.13.1", "react-bootstrap": "^0.20.3", "react-retina-image": "^1.1.2", diff --git a/src/app.js b/src/app.js index ad4e661484..2c782c1ebd 100644 --- a/src/app.js +++ b/src/app.js @@ -11,7 +11,9 @@ var metrics = require('./utils/MetricsUtil'); var router = require('./router'); var template = require('./menutemplate'); var webUtil = require('./utils/WebUtil'); -var util = require ('./utils/Util'); +var urlUtil = require ('./utils/URLUtil'); +var app = remote.require('app'); +var request = require('request'); webUtil.addWindowSizeSaving(); webUtil.addLiveReload(); @@ -28,12 +30,6 @@ setInterval(function () { router.run(Handler => React.render(, document.body)); -ipc.on('application:quitting', opts => { - if (!opts.updating && localStorage.getItem('settings.closeVMOnQuit') === 'true') { - machine.stop(); - } -}); - SetupStore.setup().then(() => { if (ContainerStore.pending()) { router.transitionTo('pull'); @@ -60,26 +56,15 @@ ipc.on('application:quitting', () => { } }); +// Event fires when the app receives a docker:// URL such as +// docker://repository/run/redis ipc.on('application:open-url', opts => { - var parser = document.createElement('a'); - parser.href = opts.url; - - if (parser.protocol !== 'docker:') { - return; - } - - var pathname = parser.pathname.replace('//', ''); - var tokens = pathname.split('/'); - var type = tokens[0]; - var method = tokens[1]; - var repo = tokens.slice(2).join('/'); - - // Only accept official repos for now - if (!util.isOfficialRepo(repo)) { - return; - } - - if (type === 'repository' && method === 'run') { - ContainerStore.setPending(repo, 'latest'); - } + request.get('https://kitematic.com/flags.json', (flags, err) => { + if (err || !flags) { + return; + } + urlUtil.openUrl(opts.url, flags, app.getVersion()); + }); }); + +urlUtil.openUrl('docker://repository/run/redis', {dockerURLEnabledVersion: '0.5.19'}, app.getVersion()); diff --git a/src/stores/SetupStore-test.js b/src/stores/SetupStore-test.js index 38bd33dec4..17bf010474 100644 --- a/src/stores/SetupStore-test.js +++ b/src/stores/SetupStore-test.js @@ -21,7 +21,7 @@ describe('SetupStore', function () { pit('downloads virtualbox if it is installed but has an outdated version', function () { virtualBox.installed.mockReturnValue(true); virtualBox.version.mockReturnValue(Promise.resolve('4.3.16')); - setupUtil.compareVersions.mockReturnValue(-1); + util.compareVersions.mockReturnValue(-1); setupUtil.download.mockReturnValue(Promise.resolve()); util.packagejson.mockReturnValue({'virtualbox-filename': ''}); util.supportDir.mockReturnValue(''); @@ -50,7 +50,7 @@ describe('SetupStore', function () { pit('only installs binaries if virtualbox is installed', function () { virtualBox.installed.mockReturnValue(true); - setupUtil.compareVersions.mockReturnValue(0); + util.compareVersions.mockReturnValue(0); setupUtil.needsBinaryFix.mockReturnValue(true); return setupStore.steps().install.run().then(() => { expect(util.exec).toBeCalledWith('macsudo copycmd && fixcmd'); @@ -68,7 +68,7 @@ describe('SetupStore', function () { machine.stop.mockReturnValue(Promise.resolve()); machine.start.mockReturnValue(Promise.resolve()); machine.upgrade.mockReturnValue(Promise.resolve()); - setupUtil.compareVersions.mockReturnValue(-1); + util.compareVersions.mockReturnValue(-1); machine.create.mockClear(); machine.upgrade.mockClear(); machine.start.mockClear(); diff --git a/src/stores/SetupStore.js b/src/stores/SetupStore.js index 6bdb76033a..10caa0d4a3 100644 --- a/src/stores/SetupStore.js +++ b/src/stores/SetupStore.js @@ -76,7 +76,7 @@ var _steps = [{ var isoversion = machine.isoversion(); var packagejson = util.packagejson(); - if (!isoversion || setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0) { + if (!isoversion || util.compareVersions(isoversion, packagejson['docker-version']) < 0) { yield machine.start(); yield machine.upgrade(); } @@ -152,10 +152,10 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { var vboxNeedsInstall = !virtualBox.installed(); required.download = vboxNeedsInstall && (!fs.existsSync(vboxfile) || setupUtil.checksum(vboxfile) !== packagejson['virtualbox-checksum']); required.install = vboxNeedsInstall || setupUtil.needsBinaryFix(); - required.init = required.install || !(yield machine.exists()) || (yield machine.state()) !== 'Running' || !isoversion || setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0; + required.init = required.install || !(yield machine.exists()) || (yield machine.state()) !== 'Running' || !isoversion || util.compareVersions(isoversion, packagejson['docker-version']) < 0; var exists = yield machine.exists(); - if (isoversion && setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0) { + if (isoversion && util.compareVersions(isoversion, packagejson['docker-version']) < 0) { this.steps().init.seconds = 33; } else if (exists && (yield machine.state()) === 'Saved') { this.steps().init.seconds = 8; diff --git a/src/utils/SetupUtil.js b/src/utils/SetupUtil.js index 95fe6c0b5f..5fe0aa10f4 100644 --- a/src/utils/SetupUtil.js +++ b/src/utils/SetupUtil.js @@ -108,55 +108,6 @@ var SetupUtil = { resolve(); }); }); - }, - compareVersions: function (v1, v2, options) { - var lexicographical = options && options.lexicographical, - zeroExtend = options && options.zeroExtend, - v1parts = v1.split('.'), - v2parts = v2.split('.'); - - function isValidPart(x) { - return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x); - } - - if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { - return NaN; - } - - if (zeroExtend) { - while (v1parts.length < v2parts.length) { - v1parts.push('0'); - } - while (v2parts.length < v1parts.length) { - v2parts.push('0'); - } - } - - if (!lexicographical) { - v1parts = v1parts.map(Number); - v2parts = v2parts.map(Number); - } - - for (var i = 0; i < v1parts.length; ++i) { - if (v2parts.length === i) { - return 1; - } - if (v1parts[i] === v2parts[i]) { - continue; - } - else if (v1parts[i] > v2parts[i]) { - return 1; - } - else { - return -1; - } - } - - if (v1parts.length !== v2parts.length) { - return -1; - } - - return 0; } }; diff --git a/src/utils/URLUtil-test.js b/src/utils/URLUtil-test.js new file mode 100644 index 0000000000..717729e5c4 --- /dev/null +++ b/src/utils/URLUtil-test.js @@ -0,0 +1,53 @@ +jest.dontMock('./URLUtil'); +jest.dontMock('parseUri'); +var urlUtil = require('./URLUtil'); +var util = require('./Util'); + +describe('URLUtil', function () { + beforeEach(() => { + util.compareVersions.mockClear(); + util.isOfficialRepo.mockClear(); + }); + + it('does nothing if the url is undefined', () => { + util.compareVersions.mockReturnValue(1); + util.isOfficialRepo.mockReturnValue(true); + expect(urlUtil.openUrl()).toBe(false); + }); + + it('does nothing if the flags are undefined', () => { + util.compareVersions.mockReturnValue(1); + util.isOfficialRepo.mockReturnValue(true); + expect(urlUtil.openUrl('docker://repository/run/redis')).toBe(false); + }); + + it('does nothing if the url enabled flag is falsy', () => { + util.compareVersions.mockReturnValue(1); + util.isOfficialRepo.mockReturnValue(true); + expect(urlUtil.openUrl('docker://repository/run/redis', {dockerURLEnabledVersion: undefined})).toBe(false); + }); + + it('does nothing if the url enabled flag is less than the flag version', () => { + util.compareVersions.mockReturnValue(-1); + util.isOfficialRepo.mockReturnValue(true); + expect(urlUtil.openUrl('docker://repository/run/redis', {dockerURLEnabledVersion: '0.5.19'}, '0.5.18')).toBe(false); + }); + + it('does nothing if protocol is not docker:', () => { + util.compareVersions.mockReturnValue(1); + util.isOfficialRepo.mockReturnValue(true); + expect(urlUtil.openUrl('facetime://')).toBe(false); + }); + + it('does nothing if repo is not official', () => { + util.compareVersions.mockReturnValue(1); + util.isOfficialRepo.mockReturnValue(false); + expect(urlUtil.openUrl('docker://repository/run/not/official', {dockerURLEnabledVersion: '0.5.19'}, '0.5.20')).toBe(false); + }); + + it('returns true if type and method are correct', () => { + util.compareVersions.mockReturnValue(1); + util.isOfficialRepo.mockReturnValue(true); + expect(urlUtil.openUrl('docker://repository/run/redis', {dockerURLEnabledVersion: '0.5.19'}, '0.5.20')).toBe(true); + }); +}); diff --git a/src/utils/URLUtil.js b/src/utils/URLUtil.js new file mode 100644 index 0000000000..4e0bd8367a --- /dev/null +++ b/src/utils/URLUtil.js @@ -0,0 +1,44 @@ +var util = require('./Util'); +var parseUri = require('parseUri'); +var containerStore = require('../Stores/ContainerStore'); + +module.exports = { + openUrl: function (url, flags, appVersion) { + if (!url || !flags || !flags.dockerURLEnabledVersion) { + return false; + } + + // Make sure this feature is enabled via the feature flag + if (util.compareVersions(appVersion, flags.dockerURLEnabledVersion) < 0) { + return false; + } + + var parser = parseUri(url); + + if (parser.protocol !== 'docker') { + return false; + } + + // Get the type of object we're operating on, e.g. 'repository' + var type = parser.host; + + // Separate the path into [run', 'redis'] + var tokens = parser.path.replace('/', '').split('/'); + + // Get the method trying to be executed, e.g. 'run' + var method = tokens[0]; + + // Get the repository namespace and repo name, e.g. 'redis' or 'myusername/myrepo' + var repo = tokens.slice(1).join('/'); + + // Only accept official repos for now + if (!util.isOfficialRepo(repo)) { + return false; + } + + if (type === 'repository' && method === 'run') { + containerStore.setPending(repo, 'latest'); + return true; + } + } +}; diff --git a/src/utils/Util.js b/src/utils/Util.js index 47cc5ad6fa..cc4fff1712 100644 --- a/src/utils/Util.js +++ b/src/utils/Util.js @@ -59,5 +59,54 @@ module.exports = { var repoRegexp = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/; return repoRegexp.test(name); }, + 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; + }, webPorts: ['80', '8000', '8080', '3000', '5000', '2368', '9200', '8983'] }; From 2268690bc68301c595c150583aa8cdfdb1fee6aa Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Mon, 27 Apr 2015 11:30:45 -0400 Subject: [PATCH 5/9] removing test url --- src/app.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app.js b/src/app.js index 2c782c1ebd..5ec68f4bb4 100644 --- a/src/app.js +++ b/src/app.js @@ -66,5 +66,3 @@ ipc.on('application:open-url', opts => { urlUtil.openUrl(opts.url, flags, app.getVersion()); }); }); - -urlUtil.openUrl('docker://repository/run/redis', {dockerURLEnabledVersion: '0.5.19'}, app.getVersion()); From e2b4261ac922c9584df71f9936b95106a510664f Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Mon, 27 Apr 2015 11:43:22 -0400 Subject: [PATCH 6/9] removing test url, console logs --- src/components/Containers.react.js | 1 - src/utils/URLUtil.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Containers.react.js b/src/components/Containers.react.js index d29cf38c19..5549462f50 100644 --- a/src/components/Containers.react.js +++ b/src/components/Containers.react.js @@ -71,7 +71,6 @@ var Containers = React.createClass({ this.context.router.transitionTo('containerHome', {name: name}); } else if (status === 'pending' && ContainerStore.pending()) { this.context.router.transitionTo('pull'); - this.context.router.transitionTo('containerHome', {name: name}); } else if (status === 'destroy') { this.onDestroy(); } diff --git a/src/utils/URLUtil.js b/src/utils/URLUtil.js index 4e0bd8367a..21461e75b6 100644 --- a/src/utils/URLUtil.js +++ b/src/utils/URLUtil.js @@ -1,6 +1,6 @@ var util = require('./Util'); var parseUri = require('parseUri'); -var containerStore = require('../Stores/ContainerStore'); +var containerStore = require('../stores/ContainerStore'); module.exports = { openUrl: function (url, flags, appVersion) { From 2ed92d9d228845ea890985bd7d2621b22cd0dc94 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Mon, 27 Apr 2015 12:34:58 -0400 Subject: [PATCH 7/9] Fixing a few small button bugs --- package.json | 2 +- src/app.js | 5 +++-- src/components/ContainerListNewItem.react.js | 7 +++++-- src/components/Containers.react.js | 4 ++-- src/components/NewContainerPull.react.js | 2 +- src/stores/ContainerStore.js | 1 + 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 35ae89fd23..aaee45e253 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Kitematic", - "version": "0.5.19", + "version": "0.5.20", "author": "Kitematic", "description": "Simple Docker Container management for Mac OS X.", "homepage": "https://kitematic.com/", diff --git a/src/app.js b/src/app.js index 5ec68f4bb4..c59815c5a2 100644 --- a/src/app.js +++ b/src/app.js @@ -59,10 +59,11 @@ ipc.on('application:quitting', () => { // Event fires when the app receives a docker:// URL such as // docker://repository/run/redis ipc.on('application:open-url', opts => { - request.get('https://kitematic.com/flags.json', (flags, err) => { - if (err || !flags) { + request.get('https://kitematic.com/flags.json', (err, response, body) => { + if (err || response.statusCode !== 200) { return; } + var flags = JSON.parse(body); urlUtil.openUrl(opts.url, flags, app.getVersion()); }); }); diff --git a/src/components/ContainerListNewItem.react.js b/src/components/ContainerListNewItem.react.js index d26c98ef32..ab64e5792e 100644 --- a/src/components/ContainerListNewItem.react.js +++ b/src/components/ContainerListNewItem.react.js @@ -5,6 +5,9 @@ var ContainerStore = require('../stores/ContainerStore'); var metrics = require('../utils/MetricsUtil'); var ContainerListNewItem = React.createClass({ + contextTypes: { + router: React.PropTypes.func + }, handleItemMouseEnter: function () { var $action = $(this.getDOMNode()).find('.action'); $action.show(); @@ -20,10 +23,10 @@ var ContainerListNewItem = React.createClass({ type: 'new' }); var containers = ContainerStore.sorted(); - $(self.getDOMNode()).fadeOut(300, function () { + $(self.getDOMNode()).fadeOut(300, () => { if (containers.length > 0) { var name = containers[0].Name; - self.transitionTo('containerHome', {name: name}); + this.context.router.transitionTo('containerHome', {name: name}); } }); }, diff --git a/src/components/Containers.react.js b/src/components/Containers.react.js index 5549462f50..810d9f4a32 100644 --- a/src/components/Containers.react.js +++ b/src/components/Containers.react.js @@ -59,9 +59,9 @@ var Containers = React.createClass({ }); if (status === 'destroy') { if (sorted.length) { - this.transitionTo('containerHome', {name: sorted[0].Name}); + this.context.router.transitionTo('containerHome', {name: sorted[0].Name}); } else { - this.transitionTo('containers'); + this.context.router.transitionTo('containers'); } } }, diff --git a/src/components/NewContainerPull.react.js b/src/components/NewContainerPull.react.js index e4da3fd9a3..33a3fcedc9 100644 --- a/src/components/NewContainerPull.react.js +++ b/src/components/NewContainerPull.react.js @@ -15,7 +15,7 @@ module.exports = React.createClass({ }, handleCancelClick: function () { ContainerStore.clearPending(); - this.transitionTo('new'); + this.context.router.transitionTo('new'); }, handleConfirmClick: function () { ContainerStore.clearPending(); diff --git a/src/stores/ContainerStore.js b/src/stores/ContainerStore.js index 0d24c12530..6dcab31b03 100644 --- a/src/stores/ContainerStore.js +++ b/src/stores/ContainerStore.js @@ -307,6 +307,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { var containerName = this._generateName(repository); _placeholders[containerName] = { + Id: require('crypto').randomBytes(32).toString('hex'), Name: containerName, Image: imageName, Config: { From f396ea5d0c07c68e890860647a00a3c018f64426 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Mon, 27 Apr 2015 13:47:44 -0400 Subject: [PATCH 8/9] Addressing some comments --- src/app.js | 6 ++++++ src/utils/URLUtil-test.js | 16 ++++++++++++++-- src/utils/URLUtil.js | 18 +++++++++++++++++- src/utils/Util.js | 5 +++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/app.js b/src/app.js index c59815c5a2..b5073ba728 100644 --- a/src/app.js +++ b/src/app.js @@ -12,6 +12,7 @@ var router = require('./router'); var template = require('./menutemplate'); var webUtil = require('./utils/WebUtil'); var urlUtil = require ('./utils/URLUtil'); +var util = require('./Util'); var app = remote.require('app'); var request = require('request'); @@ -63,7 +64,12 @@ ipc.on('application:open-url', opts => { if (err || response.statusCode !== 200) { return; } + var flags = JSON.parse(body); + if (!flags) { + return; + } + urlUtil.openUrl(opts.url, flags, app.getVersion()); }); }); diff --git a/src/utils/URLUtil-test.js b/src/utils/URLUtil-test.js index 717729e5c4..cfa137770d 100644 --- a/src/utils/URLUtil-test.js +++ b/src/utils/URLUtil-test.js @@ -15,7 +15,7 @@ describe('URLUtil', function () { expect(urlUtil.openUrl()).toBe(false); }); - it('does nothing if the flags are undefined', () => { + it('does nothing if the flags object is undefined', () => { util.compareVersions.mockReturnValue(1); util.isOfficialRepo.mockReturnValue(true); expect(urlUtil.openUrl('docker://repository/run/redis')).toBe(false); @@ -27,12 +27,24 @@ describe('URLUtil', function () { expect(urlUtil.openUrl('docker://repository/run/redis', {dockerURLEnabledVersion: undefined})).toBe(false); }); - it('does nothing if the url enabled flag is less than the flag version', () => { + it('does nothing if the url enabled flag version is higher than the app version', () => { util.compareVersions.mockReturnValue(-1); util.isOfficialRepo.mockReturnValue(true); expect(urlUtil.openUrl('docker://repository/run/redis', {dockerURLEnabledVersion: '0.5.19'}, '0.5.18')).toBe(false); }); + it('does nothing if the type is not in the whitelist', () => { + util.compareVersions.mockReturnValue(1); + util.isOfficialRepo.mockReturnValue(true); + expect(urlUtil.openUrl('docker://badtype/run/redis', {dockerURLEnabledVersion: '0.5.19'}, '0.5.18')).toBe(false); + }); + + it('does nothing if the method is not in the whitelist', () => { + util.compareVersions.mockReturnValue(1); + util.isOfficialRepo.mockReturnValue(true); + expect(urlUtil.openUrl('docker://repository/badmethod/redis', {dockerURLEnabledVersion: '0.5.19'}, '0.5.18')).toBe(false); + }); + it('does nothing if protocol is not docker:', () => { util.compareVersions.mockReturnValue(1); util.isOfficialRepo.mockReturnValue(true); diff --git a/src/utils/URLUtil.js b/src/utils/URLUtil.js index 21461e75b6..9f195ffdb4 100644 --- a/src/utils/URLUtil.js +++ b/src/utils/URLUtil.js @@ -3,8 +3,10 @@ var parseUri = require('parseUri'); var containerStore = require('../stores/ContainerStore'); module.exports = { + TYPE_WHITELIST: ['repository'], + METHOD_WHITELIST: ['run'], openUrl: function (url, flags, appVersion) { - if (!url || !flags || !flags.dockerURLEnabledVersion) { + if (!url || !flags || !flags.dockerURLEnabledVersion || !appVersion) { return false; } @@ -22,15 +24,28 @@ module.exports = { // Get the type of object we're operating on, e.g. 'repository' var type = parser.host; + if (this.TYPE_WHITELIST.indexOf(type) === -1) { + return false; + } + // Separate the path into [run', 'redis'] var tokens = parser.path.replace('/', '').split('/'); // Get the method trying to be executed, e.g. 'run' var method = tokens[0]; + if (this.METHOD_WHITELIST.indexOf(method) === -1) { + return false; + } + // Get the repository namespace and repo name, e.g. 'redis' or 'myusername/myrepo' var repo = tokens.slice(1).join('/'); + // Only accept official repos for now (one component) + if (tokens > 1) { + return false; + } + // Only accept official repos for now if (!util.isOfficialRepo(repo)) { return false; @@ -40,5 +55,6 @@ module.exports = { containerStore.setPending(repo, 'latest'); return true; } + return false; } }; diff --git a/src/utils/Util.js b/src/utils/Util.js index cc4fff1712..4473549441 100644 --- a/src/utils/Util.js +++ b/src/utils/Util.js @@ -56,6 +56,11 @@ module.exports = { if (!name || !name.length) { return false; } + + // An official repo is alphanumeric characters separated by dashes or + // underscores. + // Examples: myrepo, my-docker-repo, my_docker_repo + // Non-exapmles: mynamespace/myrepo, my%!repo var repoRegexp = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/; return repoRegexp.test(name); }, From e56f9b6c946b10db59f4fa17157805af90c2dadc Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Mon, 27 Apr 2015 17:13:55 -0400 Subject: [PATCH 9/9] Removing deadcode --- src/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app.js b/src/app.js index b5073ba728..5fdac16168 100644 --- a/src/app.js +++ b/src/app.js @@ -12,7 +12,6 @@ var router = require('./router'); var template = require('./menutemplate'); var webUtil = require('./utils/WebUtil'); var urlUtil = require ('./utils/URLUtil'); -var util = require('./Util'); var app = remote.require('app'); var request = require('request');