зеркало из https://github.com/docker/kitematic.git
Merge pull request #383 from kitematic/revert-382-revert-374-hub-button
Adding back custom URL for Hub-Button
This commit is contained in:
Коммит
7d563d06d4
|
@ -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/",
|
||||
|
@ -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",
|
||||
|
|
29
src/app.js
29
src/app.js
|
@ -11,6 +11,9 @@ var metrics = require('./utils/MetricsUtil');
|
|||
var router = require('./router');
|
||||
var template = require('./menutemplate');
|
||||
var webUtil = require('./utils/WebUtil');
|
||||
var urlUtil = require ('./utils/URLUtil');
|
||||
var app = remote.require('app');
|
||||
var request = require('request');
|
||||
|
||||
webUtil.addWindowSizeSaving();
|
||||
webUtil.addLiveReload();
|
||||
|
@ -28,13 +31,16 @@ setInterval(function () {
|
|||
router.run(Handler => React.render(<Handler/>, document.body));
|
||||
|
||||
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',
|
||||
|
@ -49,3 +55,20 @@ ipc.on('application:quitting', () => {
|
|||
machine.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// 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', (err, response, body) => {
|
||||
if (err || response.statusCode !== 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
var flags = JSON.parse(body);
|
||||
if (!flags) {
|
||||
return;
|
||||
}
|
||||
|
||||
urlUtil.openUrl(opts.url, flags, app.getVersion());
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ var shell = require('shell');
|
|||
var util = require('../utils/Util');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
var ContainerStore = require('../stores/ContainerStore');
|
||||
var dialog = require('remote').require('dialog');
|
||||
|
||||
var ContainerHomeFolder = React.createClass({
|
||||
contextTypes: {
|
||||
|
@ -17,20 +18,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);
|
||||
|
|
|
@ -21,7 +21,7 @@ var ContainerList = React.createClass({
|
|||
});
|
||||
return (
|
||||
<ul>
|
||||
<ContainerListNewItem key={'newcontainer'} containers={this.props.containers} />
|
||||
<ContainerListNewItem key={'newcontainer'} containers={this.props.containers}/>
|
||||
{containers}
|
||||
</ul>
|
||||
);
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -33,10 +33,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.context.router.transitionTo('containerHome', {name: this.state.sorted[0].Name});
|
||||
}
|
||||
|
||||
ipc.on('application:update-available', () => {
|
||||
this.setState({
|
||||
updateAvailable: true
|
||||
|
@ -48,36 +44,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.context.router.transitionTo('containerHome', {name: this.state.sorted[0].Name});
|
||||
} else {
|
||||
this.context.router.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.context.router.transitionTo('containerHome', {name: sorted[0].Name});
|
||||
} else {
|
||||
this.context.router.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.context.router.transitionTo('containerHome', {name: name});
|
||||
} else if (status === 'pending' && ContainerStore.pending()) {
|
||||
this.context.router.transitionTo('pull');
|
||||
} else if (status === 'destroy') {
|
||||
this.onDestroy();
|
||||
}
|
||||
|
@ -186,7 +179,7 @@ var Containers = React.createClass({
|
|||
<div className="sidebar-buttons-padding"></div>
|
||||
</section>
|
||||
</div>
|
||||
<Router.RouteHandler container={container} error={this.state.error}/>
|
||||
<Router.RouteHandler pending={this.state.pending} container={container} error={this.state.error}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
var React = require('react/addons');
|
||||
var Router = require('react-router');
|
||||
var shell = require('shell');
|
||||
var ContainerStore = require('../stores/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.context.router.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 (
|
||||
<div className="details">
|
||||
<div className="new-container-pull">
|
||||
<div className="content">
|
||||
<h1>You're about to download and run <a onClick={this.handleOpenClick}>{this.props.pending.repository}:{this.props.pending.tag}</a>.</h1>
|
||||
<h1>Please confirm to create the container.</h1>
|
||||
<div className="buttons">
|
||||
<a className="btn btn-action" onClick={this.handleCancelClick}>Cancel</a> <a onClick={this.handleConfirmClick} className="btn btn-action">Confirm</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,168 @@
|
|||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var React = require('react/addons');
|
||||
var RetinaImage = require('react-retina-image');
|
||||
var Radial = require('./Radial.react');
|
||||
var ImageCard = require('./ImageCard.react');
|
||||
var Promise = require('bluebird');
|
||||
var metrics = require('../utils/MetricsUtil');
|
||||
|
||||
var _recommended = [];
|
||||
var _searchPromise = null;
|
||||
|
||||
module.exports = React.createClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
query: '',
|
||||
loading: false,
|
||||
results: _recommended
|
||||
};
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.refs.searchInput.getDOMNode().focus();
|
||||
this.recommended();
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
if (_searchPromise) {
|
||||
_searchPromise.cancel();
|
||||
}
|
||||
},
|
||||
search: function (query) {
|
||||
if (_searchPromise) {
|
||||
_searchPromise.cancel();
|
||||
_searchPromise = null;
|
||||
}
|
||||
|
||||
if (!query.length) {
|
||||
this.setState({
|
||||
query: query,
|
||||
results: _recommended,
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
query: query,
|
||||
loading: true
|
||||
});
|
||||
|
||||
_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,
|
||||
query: query,
|
||||
loading: false
|
||||
});
|
||||
_searchPromise = null;
|
||||
}).catch(Promise.CancellationError, () => {
|
||||
});
|
||||
},
|
||||
recommended: function () {
|
||||
if (_recommended.length) {
|
||||
return;
|
||||
}
|
||||
Promise.resolve($.ajax({
|
||||
url: 'https://kitematic.com/recommended.json',
|
||||
cache: false,
|
||||
dataType: 'json',
|
||||
})).then(res => res.repos).map(repo => {
|
||||
var query = repo.repo;
|
||||
var vals = query.split('/');
|
||||
if (vals.length === 1) {
|
||||
query = 'library/' + vals[0];
|
||||
}
|
||||
return $.get('https://registry.hub.docker.com/v1/repositories_info/' + query).then(data => {
|
||||
var res = _.extend(data, repo);
|
||||
res.description = data.short_description;
|
||||
res.is_official = data.namespace === 'library';
|
||||
res.name = data.repo;
|
||||
res.star_count = data.stars;
|
||||
return res;
|
||||
});
|
||||
}).then(results => {
|
||||
_recommended = results.filter(r => !!r);
|
||||
if (!this.state.query.length && this.isMounted()) {
|
||||
this.setState({
|
||||
results: _recommended
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
},
|
||||
handleChange: function (e) {
|
||||
var query = e.target.value;
|
||||
if (query === this.state.query) {
|
||||
return;
|
||||
}
|
||||
this.search(query);
|
||||
},
|
||||
render: function () {
|
||||
var title = this.state.query ? 'Results' : 'Recommended';
|
||||
var data = this.state.results;
|
||||
var results;
|
||||
if (data.length) {
|
||||
var items = data.map(function (image) {
|
||||
return (
|
||||
<ImageCard key={image.name} image={image} />
|
||||
);
|
||||
});
|
||||
|
||||
results = (
|
||||
<div className="result-grid">
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (this.state.results.length === 0 && this.state.query === '') {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<div className="loader">
|
||||
<h2>Loading Images</h2>
|
||||
<Radial spin="true" progress={90} thick={true} transparent={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
results = (
|
||||
<div className="no-results">
|
||||
<h1>Cannot find a matching image.</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
var loadingClasses = React.addons.classSet({
|
||||
hidden: !this.state.loading,
|
||||
loading: true
|
||||
});
|
||||
var magnifierClasses = React.addons.classSet({
|
||||
hidden: this.state.loading,
|
||||
icon: true,
|
||||
'icon-magnifier': true,
|
||||
'search-icon': true
|
||||
});
|
||||
return (
|
||||
<div className="details">
|
||||
<div className="new-container">
|
||||
<div className="new-container-header">
|
||||
<div className="text">
|
||||
Select a Docker image to create a new container.
|
||||
</div>
|
||||
<div className="search">
|
||||
<div className="search-bar">
|
||||
<input type="search" ref="searchInput" className="form-control" placeholder="Search Docker Hub for an image" onChange={this.handleChange}/>
|
||||
<div className={magnifierClasses}></div>
|
||||
<RetinaImage className={loadingClasses} src="loading.png"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="results">
|
||||
<h4>{title}</h4>
|
||||
{results}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -9,12 +9,14 @@ var ContainerSettingsGeneral = require('./components/ContainerSettingsGeneral.re
|
|||
var ContainerSettingsPorts = require('./components/ContainerSettingsPorts.react');
|
||||
var ContainerSettingsVolumes = require('./components/ContainerSettingsVolumes.react');
|
||||
var Preferences = require('./components/Preferences.react');
|
||||
var NewContainer = require('./components/NewContainer.react');
|
||||
var NewContainerSearch = require('./components/NewContainerSearch.react');
|
||||
var NewContainerPull = require('./components/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 = (
|
||||
<Route name="app" path="/" handler={App}>
|
||||
<Route name="containers" handler={Containers}>
|
||||
<Route name="containerDetails" path="containers/:name" handler={ContainerDetails}>
|
||||
<Route name="containerHome" path="containers/:name/home" handler={ContainerHome} />
|
||||
<Route name="containerLogs" path="containers/:name/logs" handler={ContainerLogs}/>
|
||||
<Route name="containerSettings" path="containers/:name/settings" handler={ContainerSettings}>
|
||||
<Route name="containerSettingsGeneral" path="containers/:name/settings/general" handler={ContainerSettingsGeneral}/>
|
||||
<Route name="containerSettingsPorts" path="containers/:name/settings/ports" handler={ContainerSettingsPorts}/>
|
||||
<Route name="containerSettingsVolumes" path="containers/:name/settings/volumes" handler={ContainerSettingsVolumes}/>
|
||||
<Route name="containerDetails" path="containers/details/:name" handler={ContainerDetails}>
|
||||
<Route name="containerHome" path="containers/details/:name/home" handler={ContainerHome} />
|
||||
<Route name="containerLogs" path="containers/details/:name/logs" handler={ContainerLogs}/>
|
||||
<Route name="containerSettings" path="containers/details/:name/settings" handler={ContainerSettings}>
|
||||
<Route name="containerSettingsGeneral" path="containers/details/:name/settings/general" handler={ContainerSettingsGeneral}/>
|
||||
<Route name="containerSettingsPorts" path="containers/details/:name/settings/ports" handler={ContainerSettingsPorts}/>
|
||||
<Route name="containerSettingsVolumes" path="containers/details/:name/settings/volumes" handler={ContainerSettingsVolumes}/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route name="new" path="containers/new">
|
||||
<DefaultRoute name="search" handler={NewContainerSearch}/>
|
||||
<Route name="pull" path="containers/new/pull" handler={NewContainerPull}></Route>
|
||||
</Route>
|
||||
<Route name="preferences" path="/preferences" handler={Preferences}/>
|
||||
<DefaultRoute name="new" handler={NewContainer}/>
|
||||
<Redirect to="new"/>
|
||||
</Route>
|
||||
<DefaultRoute name="setup" handler={Setup}/>
|
||||
</Route>
|
||||
|
|
|
@ -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',
|
||||
|
@ -206,6 +207,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});
|
||||
|
@ -231,7 +236,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;
|
||||
|
@ -303,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: {
|
||||
|
@ -498,6 +503,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');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
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 object is 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 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);
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
var util = require('./Util');
|
||||
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 || !appVersion) {
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (type === 'repository' && method === 'run') {
|
||||
containerStore.setPending(repo, 'latest');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -2,7 +2,7 @@ jest.dontMock('./Util');
|
|||
var util = require('./Util');
|
||||
|
||||
describe('Util', function () {
|
||||
describe('removeSensitiveData', function () {
|
||||
describe('when removing sensitive data', function () {
|
||||
it('filters ssh certificate data', function () {
|
||||
var testdata = String.raw`time="2015-04-17T21:43:47-04:00" level="debug" msg="executing: ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectionAttempts=30 -o LogLevel=quiet -p 50483 -i /Users/johnappleseed/.docker/machine/machines/dev2/id_rsa docker@localhost sudo mkdir -p /var/lib/boot2docker" time="2015-04-17T21:43:47-04:00" level="debug" msg="executing: ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectionAttempts=30 -o LogLevel=quiet -p 50483 -i /Users/johnappleseed/.docker/machine/machines/dev2/id_rsa docker@localhost echo \"-----BEGIN CERTIFICATE-----\nMIIC+DCCAeKgAwIBAgIRANfIbsa2M94gDY+fBiBiQBkwCwYJKoZIhvcNAQELMBIx\nEDAOBgNVBAoTB2ptb3JnYW4wHhcNMTUwNDE4MDEzODAwWhcNMTgwNDAyMDEzODAw\nWjAPMQ0wCwYDVQQKEwRkZXYyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEA1yamWT0bk0pRU7eiStjiXe2jkzdeI0SdJZo+bjczkl6kzNW/FmR/OkcP8gHX\nCO3fUCWkR/+rBgz3nuM1Sy0BIUo0EMQGfx17OqIJPXO+BrpCHsXlphHmbQl5bE2Y\nF+bAsGc6WCippw/caNnIHRsb6zAZVYX2AHLYY0fwIDAQABo1AwTjAOBgNVHQ8BAf8EBAMCAKAwHQYD\nVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDwYDVR0R\nBAgwBocEwKhjZTALBgkqhkiG9w0BAQsDggEBAKBdD86+kl4X1VMjgGlNYnc42tWa\nbo1iDl/frxiLkfPSc2McAOm3AqX1ao+ynjqq1XTlBLPTQByu/oNZgA724LRJDfdG\nCKGUV8latW7rB1yhf/SZSmyhNjufuWlgCtbkw7Q/oPddzYuSOdDW8tVok9gMC0vL\naqKCWfVKkCmvGH+8/wPrkYmro/f0uwJ8ee+yrbBPlBE/qE+Lqcfr0YcXEDaS8CmL\nDjWg7KNFpA6M+/tFNQhplbjwRsCt7C4bzQu0aBIG5XH1Jr2HrKlLjWdmluPHWUL6\nX5Vh1bslYJzsSdBNZFWSKShZ+gtRpjtV7NynANDJPQNIRhDxAf4uDY9hA2c=\n-----END CERTIFICATE-----\n\" | sudo tee /var/lib/boot2docker/server.pem"
|
||||
time="2015-04-17T21:43:47-04:00" level="debug" msg="executing: /usr/bin/VBoxManage showvminfo dev2 --machinereadable"`;
|
||||
|
@ -32,4 +32,26 @@ describe('Util', function () {
|
|||
expect(util.removeSensitiveData(undefined)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when verifying that a repo is official', 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -52,5 +52,66 @@ module.exports = {
|
|||
} catch (err) {}
|
||||
return settingsjson;
|
||||
},
|
||||
isOfficialRepo: function (name) {
|
||||
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);
|
||||
},
|
||||
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']
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -134,7 +134,6 @@ input[type="text"] {
|
|||
font-weight: 400;
|
||||
text-shadow: none;
|
||||
padding: 5px 14px 5px 14px;
|
||||
height: 30px;
|
||||
cursor: default;
|
||||
|
||||
&.small {
|
||||
|
|
|
@ -26,5 +26,16 @@
|
|||
<string>AtomApplication</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>docker</string>
|
||||
</array>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Docker App Protocol</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
Загрузка…
Ссылка в новой задаче