Merge branch 'master' into clickable-legends

This commit is contained in:
Shelby Sturgis 2015-07-16 10:18:41 -04:00
commit d1c3b14906
36 changed files with 2207 additions and 177 deletions

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ target
*.log
esvm
.htpasswd
src/server/bin/plugins

View file

@ -35,7 +35,9 @@ module.exports = function (grunt) {
'Gruntfile.js',
'<%= root %>/tasks/**/*.js',
'<%= src %>/kibana/*.js',
'<%= src %>/server/**/*.js',
'<%= src %>/server/bin/*.js',
'<%= src %>/server/{config,lib,plugins}/**/*.js',
'<%= src %>/server/bin/{plugin,startup}/**/*.js',
'<%= src %>/kibana/{components,directives,factories,filters,plugins,registry,services,utils}/**/*.js',
'<%= unitTestDir %>/**/*.js',
'!<%= unitTestDir %>/specs/vislib/fixture/**/*'

View file

@ -31,7 +31,6 @@
"scripts": {
"test": "grunt test",
"start": "node ./src/server/bin/kibana.js",
"postinstall": "bower install && grunt licenses --check-validity",
"precommit": "grunt lintStagedFiles"
},
"repository": {
@ -48,6 +47,7 @@
"cookie-parser": "^1.3.3",
"debug": "^2.1.1",
"elasticsearch": "^5.0.0",
"expiry-js": "^0.1.7",
"express": "^4.10.6",
"glob": "^4.3.2",
"good": "^5.1.2",
@ -57,12 +57,13 @@
"hapi": "^8.6.1",
"joi": "^6.4.3",
"js-yaml": "^3.2.5",
"lodash": "^3.9.3",
"json-stringify-safe": "^5.0.1",
"lodash": "^3.9.3",
"moment": "^2.10.3",
"numeral": "^1.5.3",
"request": "^2.40.0",
"requirefrom": "^0.2.0",
"rimraf": "^2.4.0",
"semver": "^4.3.6",
"serve-favicon": "^2.2.0",
"through": "^2.3.6"
@ -94,14 +95,14 @@
"husky": "^0.8.1",
"istanbul": "^0.3.15",
"jade": "^1.8.2",
"license-checker": "^3.1.0",
"libesvm": "^1.0.1",
"license-checker": "^3.1.0",
"load-grunt-config": "^0.7.0",
"marked": "^0.3.3",
"marked-text-renderer": "^0.1.0",
"mkdirp": "^0.5.0",
"mocha": "^2.2.5",
"nock": "^1.6.0",
"nock": "^2.7.0",
"npm": "^2.11.0",
"opn": "^1.0.0",
"path-browserify": "0.0.0",

View file

@ -12,7 +12,7 @@ define(function (require) {
group: 'none',
name: 'orderAgg',
title: 'Order Agg',
aggFilter: ['!percentiles', '!std_dev']
aggFilter: ['!percentiles', '!median', '!std_dev']
}
])).all[0];

View file

@ -7,6 +7,7 @@ define(function (require) {
Private(require('components/agg_types/metrics/count')),
Private(require('components/agg_types/metrics/avg')),
Private(require('components/agg_types/metrics/sum')),
Private(require('components/agg_types/metrics/median')),
Private(require('components/agg_types/metrics/min')),
Private(require('components/agg_types/metrics/max')),
Private(require('components/agg_types/metrics/std_deviation')),

View file

@ -0,0 +1,29 @@
define(function (require) {
return function AggTypeMetricMaxProvider(Private) {
var _ = require('lodash');
var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type'));
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
var percentiles = Private(require('components/agg_types/metrics/percentiles'));
return new MetricAggType({
name: 'median',
dslName: 'percentiles',
title: 'Median',
makeLabel: function (aggConfig) {
return 'Median ' + aggConfig.params.field.displayName;
},
params: [
{
name: 'field',
filterFieldTypes: 'number'
},
{
name: 'percents',
default: [50]
}
],
getResponseAggs: percentiles.getResponseAggs,
getValue: percentiles.getValue
});
};
});

View file

@ -8,7 +8,7 @@ doc-table {
.flex(1, 1, 100%);
.discover-table-datafield {
white-space: pre;
white-space: pre-wrap;
}
.loading {

View file

@ -2,22 +2,28 @@ define(function (require) {
var module = require('modules').get('kibana');
var _ = require('lodash');
var rison = require('utils/rison');
var keymap = require('utils/key_map');
module.directive('savedObjectFinder', function (savedSearches, savedVisualizations, savedDashboards, $location, kbnUrl) {
var vars = {
var types = {
searches: {
service: savedSearches,
name: 'searches',
noun: 'Saved Search',
nouns: 'Saved Searches'
nouns: 'searches'
},
visualizations: {
service: savedVisualizations,
noun: 'Visualization'
name: 'visualizations',
noun: 'Visualization',
nouns: 'visualizations'
},
dashboards: {
service: savedDashboards,
noun: 'Dashboard'
name: 'dashboards',
noun: 'Dashboard',
nouns: 'dashboards'
}
};
@ -32,14 +38,17 @@ define(function (require) {
userOnChoose: '=?onChoose'
},
template: require('text!partials/saved_object_finder.html'),
link: function ($scope, $el) {
controllerAs: 'finder',
controller: function ($scope, $element, $timeout) {
var self = this;
// the text input element
var $input = $el.find('input[ng-model=filter]');
var $input = $element.find('input[ng-model=filter]');
// the list that will hold the suggestions
var $list = $el.find('.finder-options');
var $list = $element.find('ul');
// the current filter string, used to check that retured results are still useful
// the current filter string, used to check that returned results are still useful
var currentFilter = $scope.filter;
// the most recently entered search/filter
@ -48,18 +57,19 @@ define(function (require) {
// the service we will use to find records
var service;
// the currently selected jQuery element
var $selected = null;
// the list of hits, used to render display
$scope.hits = [];
self.hits = [];
self.objectType = types[$scope.type];
filterResults();
/**
* Passed the hit objects and will determine if the
* hit should have a url in the UI, returns it if so
* @return {string|null} - the url or nothing
*/
$scope.makeUrl = function (hit) {
self.makeUrl = function (hit) {
if ($scope.userMakeUrl) {
return $scope.userMakeUrl(hit);
}
@ -67,21 +77,27 @@ define(function (require) {
if (!$scope.userOnChoose) {
return hit.url;
}
return '#';
};
self.preventClick = function ($event) {
$event.preventDefault();
};
/**
* Called when a hit object is clicked, can override the
* url behavior if necessary.
*/
$scope.onChoose = function (hit, $event) {
self.onChoose = function (hit, $event) {
if ($scope.userOnChoose) {
$scope.userOnChoose(hit, $event);
}
if ($event.isDefaultPrevented()) return;
var url = $scope.makeUrl(hit);
if (!url || url.charAt(0) !== '#') return;
var url = self.makeUrl(hit);
if (!url || url === '#' || url.charAt(0) !== '#') return;
$event.preventDefault();
@ -89,14 +105,6 @@ define(function (require) {
kbnUrl.change(url.substr(1));
};
$scope.$watch('type', function (type) {
type = vars[type];
service = type.service;
$scope.noun = type.noun;
$scope.nouns = type.nouns || type.noun + 's';
filterResults();
});
$scope.$watch('filter', function (newFilter) {
// ensure that the currentFilter changes from undefined to ''
// which triggers
@ -104,100 +112,124 @@ define(function (require) {
filterResults();
});
$scope.selectedItem = false;
$input.on('keydown', (function () {
var enter = 13;
var up = 38;
var down = 40;
var left = 37;
var right = 39;
var esc = 27;
//manages the state of the keyboard selector
self.selector = {
enabled: false,
index: -1
};
var scrollIntoView = function ($el, snapTop) {
var el = $el[0];
//key handler for the filter text box
self.filterKeyDown = function ($event) {
if (keymap[$event.keyCode] !== 'tab') return;
if (!el) return;
if (self.hits.length === 0) return;
if ('scrollIntoViewIfNeeded' in el) {
el.scrollIntoViewIfNeeded(snapTop);
} else if ('scrollIntoView' in el) {
el.scrollIntoView(snapTop);
}
};
self.selector.index = 0;
self.selector.enabled = true;
return function (event) {
var $next;
var goingUp;
selectTopHit();
switch (event.keyCode) {
case enter:
if (!$selected) return;
$event.preventDefault();
};
// get the index of the selected element
var i = $list.find('li').index($selected);
//key handler for the list items
self.hitKeyDown = function ($event, page, paginate) {
switch (keymap[$event.keyCode]) {
case 'tab':
if (!self.selector.enabled) break;
// get the related hit item
var hit = $scope.hits[i];
self.selector.index = -1;
self.selector.enabled = false;
if (!hit) return;
//if the user types shift-tab return to the textbox
//if the user types tab, set the focus to the currently selected hit.
if ($event.shiftKey) {
$input.focus();
} else {
$list.find('li.active a').focus();
}
// check if there is a url for this hit
var url = $scope.makeUrl(hit);
if (url) window.location = url;
$scope.onChoose(hit);
return;
case up:
$next = $selected ? $selected.prev() : $list.find('li:last-child');
goingUp = false;
$event.preventDefault();
break;
case down:
$next = $selected ? $selected.next() : $list.find('li:first-child');
goingUp = true;
case 'down':
if (!self.selector.enabled) break;
if (self.selector.index + 1 < page.length) {
self.selector.index += 1;
}
$event.preventDefault();
break;
case esc:
scrollIntoView($list.find('li:first-child'));
$next = null;
case 'up':
if (!self.selector.enabled) break;
if (self.selector.index > 0) {
self.selector.index -= 1;
}
$event.preventDefault();
break;
case 'right':
if (!self.selector.enabled) break;
if (page.number < page.count) {
paginate.goToPage(page.number + 1);
self.selector.index = 0;
selectTopHit();
}
$event.preventDefault();
break;
case 'left':
if (!self.selector.enabled) break;
if (page.number > 1) {
paginate.goToPage(page.number - 1);
self.selector.index = 0;
selectTopHit();
}
$event.preventDefault();
break;
case 'escape':
if (!self.selector.enabled) break;
$input.focus();
$event.preventDefault();
break;
case 'enter':
if (!self.selector.enabled) break;
var hitIndex = ((page.number - 1) * paginate.perPage) + self.selector.index;
var hit = self.hits[hitIndex];
if (!hit) break;
self.onChoose(hit, $event);
$event.preventDefault();
break;
case 'shift':
break;
default:
return;
}
$input.focus();
break;
}
};
if ($next && $next.length === 0) {
// we are at one of the ends
return;
}
self.hitBlur = function ($event) {
self.selector.index = -1;
self.selector.enabled = false;
};
if ($selected && $next && $next.eq($selected).length) {
// the selections are the same, bail
return;
}
if ($selected) {
$selected.removeClass('active');
$selected = null;
}
if ($next) {
// remove selection stuff from $selected
$next.addClass('active');
scrollIntoView($next, goingUp);
$selected = $next;
}
};
}()));
$scope.$on('$destroy', function () {
$input.off('keydown');
});
$scope.manageObject = function (type) {
self.manageObjects = function (type) {
$location.url('/settings/objects?_a=' + rison.encode({tab: type}));
};
function selectTopHit() {
setTimeout(function () {
//triggering a focus event kicks off a new angular digest cycle.
$list.find('a:first').focus();
}, 0);
}
function filterResults() {
if (!service) return;
if (!self.objectType) return;
if (!self.objectType.service) return;
// track the filter that we use for this search,
// but ensure that we don't search for the same
@ -207,17 +239,28 @@ define(function (require) {
if (prevSearch === filter) return;
prevSearch = filter;
service.find(filter)
self.objectType.service.find(filter)
.then(function (hits) {
// ensure that we don't display old results
// as we can't really cancel requests
if (currentFilter === filter) {
$scope.hitCount = hits.total;
$scope.hits = hits.hits;
$selected = null;
self.hits = _.sortBy(hits.hits, 'title');
}
});
}
function scrollIntoView($element, snapTop) {
var el = $element[0];
if (!el) return;
if ('scrollIntoViewIfNeeded' in el) {
el.scrollIntoViewIfNeeded(snapTop);
} else if ('scrollIntoView' in el) {
el.scrollIntoView(snapTop);
}
}
}
};
});

View file

@ -1,30 +1,47 @@
<form role="form">
<div class="form-group finder-form">
<div class="finder-form-options">
<a class="small" ng-click="manageObject(type)">manage {{type}}</a>
<a class="small" ng-click="finder.manageObjects(finder.objectType.name)">manage {{finder.objectType.name}}</a>
</div>
<div class="clearfix visible-xs-block"></div>
<input
input-focus
ng-model="filter"
ng-attr-placeholder="{{noun}} Filter"
ng-attr-placeholder="{{finder.objectType.noun}} Filter"
ng-keydown="finder.filterKeyDown($event)"
class="form-control"
name="filter"
type="text">
<span class="finder-hit-count"><strong>{{hitCount}}</strong> {{type}}</span>
type="text" />
<span class="finder-hit-count"><strong>{{hitCount}}</strong> {{finder.objectType.name}}</span>
</div>
</form>
<paginate list="hits | orderBy:'title'" per-page="5">
<ul class="list-group list-group-menu">
<a class="list-group-item list-group-menu-item"
<paginate list="finder.hits" per-page="5">
<ul
class="list-group list-group-menu"
ng-class="{'select-mode': finder.selector.enabled}">
<li
class="list-group-item list-group-menu-item"
ng-class="{'active': finder.selector.index === $index && finder.selector.enabled}"
ng-repeat="hit in page"
ng-href="{{ makeUrl(hit) }}"
ng-click="onChoose(hit, $event)">
<li>
ng-keydown="finder.hitKeyDown($event, page, paginate)"
ng-click="finder.onChoose(hit, $event)">
<a
ng-href="{{finder.makeUrl(hit)}}"
ng-blur="finder.hitBlur($event)"
ng-click="finder.preventClick($event)">
<i aria-hidden="true" class="fa" ng-if="hit.icon" ng-class="hit.icon"></i> {{hit.title}}
<p ng-if="hit.description" ng-bind="hit.description"></p>
</li>
</a>
<p ng-if="hits.length === 0" ng-bind="'No matching ' + nouns + ' found.'"></p>
</a>
</li>
<li
class="list-group-item list-group-no-results"
ng-if="finder.hits.length === 0">
<p ng-bind="'No matching ' + finder.objectType.nouns + ' found.'"></p>
</li>
</ul>
</paginate>
</paginate>

View file

@ -225,7 +225,15 @@ notifications {
.list-group-menu {
&.select-mode a{
outline: none;
color: @link-color;
}
.list-group-menu-item {
list-style: none;
color: @link-color;
&.active {
font-weight: bold;
background-color: @well-bg;
@ -351,6 +359,10 @@ saved-object-finder {
&:first-child {
.border-top-radius(0) !important;
}
&.list-group-no-results p {
margin-bottom: 0 !important;
}
}
div.finder-form {

View file

@ -16,6 +16,4 @@ TITLE Kibana Server @@version
:finally
ENDLOCAL
ENDLOCAL

View file

@ -1,12 +1,11 @@
#!/usr/bin/env node
var _ = require('lodash');
var Kibana = require('../');
var program = require('commander');
require('../lib/commanderExtensions')(program);
var path = require('path');
var writePidFile = require('../lib/write_pid_file');
var loadSettingsFromYAML = require('../lib/load_settings_from_yaml');
var settings = { 'logging.console.json': true };
var startupOptions = require('./startup/startupOptions');
var startup = require('./startup/startup');
var pluginProgram = require('./plugin/plugin');
var env = (process.env.NODE_ENV) ? process.env.NODE_ENV : 'development';
var packagePath = path.resolve(__dirname, '..', '..', '..', 'package.json');
@ -17,51 +16,12 @@ var package = require(packagePath);
program.description('Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch.');
program.version(package.version);
program.option('-e, --elasticsearch <uri>', 'Elasticsearch instance');
program.option('-c, --config <path>', 'Path to the config file');
program.option('-p, --port <port>', 'The port to bind to', parseInt);
program.option('-q, --quiet', 'Turns off logging');
program.option('-H, --host <host>', 'The host to bind to');
program.option('-l, --log-file <path>', 'The file to log to');
program.option('--plugins <path>', 'Path to scan for plugins');
startupOptions(program);
pluginProgram(program);
program.parse(process.argv);
if (program.plugins) {
settings['kibana.externalPluginsFolder'] = program.plugins;
}
if (program.elasticsearch) {
settings['elasticsearch.url'] = program.elasticsearch;
}
if (program.port) {
settings['kibana.server.port'] = program.port;
}
if (program.host) {
settings['kibana.server.host'] = program.host;
}
if (program.quiet) {
settings['logging.quiet'] = program.quiet;
}
if (program.logFile) {
settings['logging.file'] = program.logFile;
}
var configPath = program.config || process.env.CONFIG_PATH;
if (configPath) {
settings = _.defaults(settings, loadSettingsFromYAML(configPath));
}
// Start the Kibana server with the settings fromt he CLI and YAML file
var kibana = new Kibana(settings);
kibana.listen()
.then(writePidFile)
.catch(function (err) {
process.exit(1);
});
if (!program.isCommandSpecified()) {
startup(program);
}

View file

@ -0,0 +1,36 @@
var Promise = require('bluebird');
var fs = require('fs');
var path = require('path');
var exec = require('child_process').exec;
module.exports = function (dest, logger) {
return new Promise(function (resolve, reject) {
//throw an exception if package.json does not exist
try {
var packageFile = path.join(dest, 'package.json');
fs.statSync(packageFile);
} catch (e) {
if (e.code !== 'ENOENT')
throw e;
return reject(new Error('Plugin does not contain package.json file'));
}
var cmd = '"' + path.resolve(path.dirname(process.execPath), 'npm').replace(/\\/g, '/') + '" install --production';
var child = exec(cmd, { cwd: dest });
child.on('error', function (err) {
reject(err);
});
child.on('exit', function (code, signal) {
if (code === 0) {
resolve();
} else {
reject(new Error('npm install failed with code ' + code));
}
});
logger.error(child.stderr);
logger.log(child.stdout);
});
};

View file

@ -0,0 +1,53 @@
var settingParser = require('./settingParser');
var installer = require('./pluginInstaller');
var remover = require('./pluginRemover');
var pluginLogger = require('./pluginLogger');
module.exports = function (program) {
function processCommand(command, options) {
var settings;
try {
settings = settingParser(command).parse();
} catch (ex) {
//The logger has not yet been initialized.
console.error(ex.message);
process.exit(64);
}
var logger = pluginLogger(settings);
if (settings.action === 'install') {
installer.install(settings, logger);
}
if (settings.action === 'remove') {
remover.remove(settings, logger);
}
}
var installDesc =
'The plugin to install\n\n' +
'\tCommon examples:\n' +
'\t -i username/sample\n' +
'\t attempts to download the latest version from the following urls:\n' +
'\t https://download.elastic.co/username/sample/sample-latest.tar.gz\n' +
'\t https://github.com/username/sample/archive/master.tar.gz\n\n' +
'\t -i username/sample/v1.1.1\n' +
'\t attempts to download version v1.1.1 from the following urls:\n' +
'\t https://download.elastic.co/username/sample/sample-v1.1.1.tar.gz\n' +
'\t https://github.com/username/sample/archive/v1.1.1.tar.gz\n\n' +
'\t -i sample -u http://www.example.com/other_name.tar.gz\n' +
'\t attempts to download from the specified url,\n' +
'\t and installs the plugin found at that url as "sample"' +
'\n';
program
.command('plugin')
.description('Maintain Plugins')
.option('-i, --install <org>/<plugin>/<version>', installDesc)
.option('-r, --remove <plugin>', 'The plugin to remove')
.option('-q, --quiet', 'Disable all process messaging except errors')
.option('-s, --silent', 'Disable all process messaging')
.option('-u, --url <url>', 'Specify download url')
.option('-t, --timeout <duration>', 'Length of time before failing; 0 for never fail', settingParser.parseMilliseconds)
.action(processCommand);
};

View file

@ -0,0 +1,40 @@
var rimraf = require('rimraf');
var fs = require('fs');
var Promise = require('bluebird');
module.exports = function (settings, logger) {
function cleanPrevious() {
return new Promise(function (resolve, reject) {
try {
fs.statSync(settings.workingPath);
logger.log('Found previous install attempt. Deleting...');
try {
rimraf.sync(settings.workingPath);
} catch (e) {
return reject(e);
}
return resolve();
} catch (e) {
if (e.code !== 'ENOENT')
return reject(e);
return resolve();
}
});
}
function cleanError() {
//delete the working directory.
//At this point we're bailing, so swallow any errors on delete.
try {
rimraf.sync(settings.workingPath);
} catch (e) { }
}
return {
cleanPrevious: cleanPrevious,
cleanError: cleanError
};
};

View file

@ -0,0 +1,94 @@
var _ = require('lodash');
var zlib = require('zlib');
var Promise = require('bluebird');
var request = require('request');
var tar = require('tar');
var progressReporter = require('./progressReporter');
module.exports = function (settings, logger) {
//Attempts to download each url in turn until one is successful
function download() {
var urls = settings.urls;
function tryNext() {
var sourceUrl = urls.shift();
if (!sourceUrl) {
throw new Error('Not a valid url.');
}
logger.log('attempting to download ' + sourceUrl);
return Promise.try(function () {
return downloadSingle(sourceUrl, settings.workingPath, settings.timeout, logger)
.catch(function (err) {
if (err.message === 'ENOTFOUND') {
return tryNext();
}
if (err.message === 'EEXTRACT') {
throw (new Error('Error extracting the plugin archive'));
}
throw (err);
});
})
.catch(function (err) {
//Special case for when request.get throws an exception
if (err.message.match(/invalid uri/i)) {
return tryNext();
}
throw (err);
});
}
return tryNext();
}
//Attempts to download a single url
function downloadSingle(source, dest, timeout) {
var gunzip = zlib.createGunzip();
var tarExtract = tar.Extract({ path: dest, strip: 1 });
var requestOptions = { url: source };
if (timeout !== 0) {
requestOptions.timeout = timeout;
}
return wrappedRequest(requestOptions)
.then(function (req) {
//debugger;
var reporter = progressReporter(logger, req);
req
.on('response', reporter.handleResponse)
.on('data', reporter.handleData)
.on('error', _.partial(reporter.handleError, 'ENOTFOUND'))
.pipe(gunzip)
.on('error', _.partial(reporter.handleError, 'EEXTRACT'))
.pipe(tarExtract)
.on('error', _.partial(reporter.handleError, 'EEXTRACT'))
.on('end', reporter.handleEnd);
return reporter.promise;
});
}
function wrappedRequest(requestOptions) {
//debugger;
return Promise.try(function () {
//debugger;
return request.get(requestOptions);
})
.catch(function (err) {
if (err.message.match(/invalid uri/i)) {
throw new Error('ENOTFOUND');
}
throw err;
});
}
return {
download: download,
_downloadSingle: downloadSingle
};
};

View file

@ -0,0 +1,42 @@
var pluginDownloader = require('./pluginDownloader');
var pluginCleaner = require('./pluginCleaner');
var npmInstall = require('./npmInstall');
var fs = require('fs');
module.exports = {
install: install
};
function install(settings, logger) {
logger.log('installing ' + settings.package);
try {
fs.statSync(settings.pluginPath);
logger.error('Plugin ' + settings.package + ' already exists. Please remove before installing a new version.');
process.exit(70);
} catch (e) {
if (e.code !== 'ENOENT')
throw e;
}
var cleaner = pluginCleaner(settings, logger);
var downloader = pluginDownloader(settings, logger);
return cleaner.cleanPrevious()
.then(function () {
return downloader.download();
})
.then(function () {
return npmInstall(settings.workingPath, logger);
})
.then(function (curious) {
fs.renameSync(settings.workingPath, settings.pluginPath);
logger.log('Plugin installation complete!');
})
.catch(function (e) {
logger.error('Plugin installation was unsuccessful due to error "' + e.message + '"');
cleaner.cleanError();
process.exit(70);
});
}

View file

@ -0,0 +1,44 @@
module.exports = function (settings) {
var previousLineEnded = true;
var silent = !!settings.silent;
var quiet = !!settings.quiet;
function log(data, sameLine) {
if (silent || quiet) return;
if (!sameLine && !previousLineEnded) {
process.stdout.write('\n');
}
//if data is a stream, pipe it.
if (data.readable) {
data.pipe(process.stdout);
return;
}
process.stdout.write(data);
if (!sameLine) process.stdout.write('\n');
previousLineEnded = !sameLine;
}
function error(data) {
if (silent) return;
if (!previousLineEnded) {
process.stderr.write('\n');
}
//if data is a stream, pipe it.
if (data.readable) {
data.pipe(process.stderr);
return;
}
process.stderr.write(data + '\n');
previousLineEnded = true;
}
return {
log: log,
error: error
};
};

View file

@ -0,0 +1,26 @@
var fs = require('fs');
var rimraf = require('rimraf');
module.exports = {
remove: remove
};
function remove(settings, logger) {
try {
try {
fs.statSync(settings.pluginPath);
}
catch (e) {
logger.log('Plugin ' + settings.package + ' does not exist.');
return;
}
logger.log('Removing ' + settings.package + '...');
rimraf.sync(settings.pluginPath);
} catch (err) {
var message = 'Unable to remove plugin "' + settings.package + '" because of error: "' + err.message + '"';
logger.error(message);
process.exit(74);
}
}

View file

@ -0,0 +1,71 @@
var Promise = require('bluebird');
/*
Responsible for reporting the progress of the file request stream
*/
module.exports = function (logger, request) {
var oldDotCount = 0;
var runningTotal = 0;
var totalSize = 0;
var hasError = false;
var _resolve;
var _reject;
var _resp;
var promise = new Promise(function (resolve, reject) {
_resolve = resolve;
_reject = reject;
});
function handleError(errorMessage, err) {
if (hasError) return;
if (err) logger.error(err);
hasError = true;
request.abort();
_reject(new Error(errorMessage));
}
function handleResponse(resp) {
_resp = resp;
if (resp.statusCode >= 400) {
handleError('ENOTFOUND', null);
} else {
totalSize = parseInt(resp.headers['content-length'], 10) || 0;
var totalDesc = totalSize || 'unknown number of';
logger.log('Downloading ' + totalDesc + ' bytes', true);
}
}
//Should log a dot for every 5% of progress
//Note: no progress is logged if the plugin is downloaded in a single packet
function handleData(buffer) {
if (hasError) return;
if (!totalSize) return;
runningTotal += buffer.length;
var dotCount = Math.round(runningTotal / totalSize * 100 / 5);
if (dotCount > 20) dotCount = 20;
for (var i = 0; i < (dotCount - oldDotCount) ; i++) {
logger.log('.', true);
}
oldDotCount = dotCount;
}
function handleEnd() {
if (hasError) return;
logger.log('Download Complete.');
_resolve();
}
return {
promise: promise,
handleResponse: handleResponse,
handleError: handleError,
handleData: handleData,
handleEnd: handleEnd,
hasError: function () { return hasError; }
};
};

View file

@ -0,0 +1,108 @@
var path = require('path');
var expiry = require('expiry-js');
module.exports = function (options) {
function parseMilliseconds(val) {
var result;
try {
var timeVal = expiry(val);
result = timeVal.asMilliseconds();
} catch (ex) {
result = 0;
}
return result;
}
function generateDownloadUrl(settings) {
var version = (settings.version) || 'latest';
var filename = settings.package + '-' + version + '.tar.gz';
return 'https://download.elastic.co/' + settings.organization + '/' + settings.package + '/' + filename;
}
function generateGithubUrl(settings) {
var version = (settings.version) || 'master';
var filename = version + '.tar.gz';
return 'https://github.com/' + settings.organization + '/' + settings.package + '/archive/' + filename;
}
function parse() {
var parts;
var settings = {
timeout: 0,
silent: false,
quiet: false,
urls: []
};
settings.workingPath = path.resolve(__dirname, '..', 'plugins', '.plugin.installing');
if (options.timeout) {
settings.timeout = options.timeout;
}
if (options.parent && options.parent.quiet) {
settings.quiet = options.parent.quiet;
}
if (options.silent) {
settings.silent = options.silent;
}
if (options.url) {
settings.urls.push(options.url);
}
if (options.install) {
settings.action = 'install';
parts = options.install.split('/');
if (options.url) {
if (parts.length !== 1) {
throw new Error('Invalid install option. When providing a url, please use the format <plugin>.');
}
settings.package = parts.shift();
} else {
if (parts.length < 2 || parts.length > 3) {
throw new Error('Invalid install option. Please use the format <org>/<plugin>/<version>.');
}
settings.organization = parts.shift();
settings.package = parts.shift();
settings.version = parts.shift();
settings.urls.push(generateDownloadUrl(settings));
settings.urls.push(generateGithubUrl(settings));
}
}
if (options.remove) {
settings.action = 'remove';
parts = options.remove.split('/');
if (parts.length !== 1) {
throw new Error('Invalid remove option. Please use the format <plugin>.');
}
settings.package = parts.shift();
}
if (!settings.action || (options.install && options.remove)) {
throw new Error('Please specify either --install or --remove.');
}
if (settings.package) {
settings.pluginPath = path.resolve(__dirname, '..', 'plugins', settings.package);
}
return settings;
}
return {
parse: parse,
parseMilliseconds: parseMilliseconds
};
};

View file

@ -0,0 +1,49 @@
module.exports = function (program) {
var _ = require('lodash');
var path = require('path');
var Kibana = require('../../');
var writePidFile = require('../../lib/write_pid_file');
var loadSettingsFromYAML = require('../../lib/load_settings_from_yaml');
var settings = { 'logging.console.json': true };
function parseSettings() {
if (program.plugins) {
settings['kibana.externalPluginsFolder'] = program.plugins;
}
if (program.elasticsearch) {
settings['elasticsearch.url'] = program.elasticsearch;
}
if (program.port) {
settings['kibana.server.port'] = program.port;
}
if (program.host) {
settings['kibana.server.host'] = program.host;
}
if (program.quiet) {
settings['logging.quiet'] = program.quiet;
}
if (program.logFile) {
settings['logging.file'] = program.logFile;
}
var configPath = program.config || process.env.CONFIG_PATH;
if (configPath) {
settings = _.defaults(settings, loadSettingsFromYAML(configPath));
}
}
parseSettings();
// Start the Kibana server with the settings fromt he CLI and YAML file
var kibana = new Kibana(settings);
kibana.listen()
.then(writePidFile)
.catch(function (err) {
process.exit(1);
});
};

View file

@ -0,0 +1,9 @@
module.exports = function (program) {
program.option('-e, --elasticsearch <uri>', 'Elasticsearch instance');
program.option('-c, --config <path>', 'Path to the config file');
program.option('-p, --port <port>', 'The port to bind to', parseInt);
program.option('-q, --quiet', 'Turns off logging');
program.option('-H, --host <host>', 'The host to bind to');
program.option('-l, --log-file <path>', 'The file to log to');
program.option('--plugins <path>', 'Path to scan for plugins');
};

View file

@ -0,0 +1,9 @@
module.exports = function (program) {
function isCommand(val) {
return typeof val === 'object' && val._name;
}
program.isCommandSpecified = function () {
return program.args.some(isCommand);
};
};

View file

@ -0,0 +1,69 @@
var root = require('requirefrom')('');
var expect = require('expect.js');
var nock = require('nock');
var glob = require('glob');
var rimraf = require('rimraf');
var fs = require('fs');
var join = require('path').join;
var sinon = require('sinon');
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
var npmInstall = root('src/server/bin/plugin/npmInstall');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('npmInstall', function () {
var logger;
var testWorkingPath = join(__dirname, '.test.data');
var statSyncStub;
beforeEach(function () {
statSyncStub = undefined;
logger = pluginLogger(false);
rimraf.sync(testWorkingPath);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
if (statSyncStub) statSyncStub.restore();
});
it('should throw an error if there is no package.json file in the archive', function () {
fs.mkdirSync(testWorkingPath);
var errorStub = sinon.stub();
return npmInstall(testWorkingPath, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(errorStub.lastCall.args[0].message).to.match(/package.json/);
});
});
it('should rethrow any errors other than "ENOENT" from fs.statSync', function () {
fs.mkdirSync(testWorkingPath);
statSyncStub = sinon.stub(fs, 'statSync', function () {
throw new Error('This is unexpected.');
});
var errorStub = sinon.stub();
return npmInstall(testWorkingPath, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(errorStub.lastCall.args[0].message).to.match(/This is unexpected./);
});
});
});
});
});

View file

@ -0,0 +1,80 @@
var root = require('requirefrom')('');
var plugin = root('src/server/bin/plugin/plugin');
var expect = require('expect.js');
var sinon = require('sinon');
var installer = root('src/server/bin/plugin/pluginInstaller');
var remover = root('src/server/bin/plugin/pluginRemover');
var settingParser = root('src/server/bin/plugin/settingParser');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('commander options', function () {
var program = {
command: function () { return program; },
description: function () { return program; },
option: function () { return program; },
action: function () { return program; }
};
it('should define the command', function () {
sinon.spy(program, 'command');
plugin(program);
expect(program.command.calledWith('plugin')).to.be(true);
program.command.restore();
});
it('should define the description', function () {
sinon.spy(program, 'description');
plugin(program);
expect(program.description.calledWith('Maintain Plugins')).to.be(true);
program.description.restore();
});
it('should define the command line options', function () {
var spy = sinon.spy(program, 'option');
var options = [
/-i/,
/-r/,
/-s/,
/-u/,
/-t/
];
plugin(program);
for (var i = 0; i < spy.callCount; i++) {
var call = spy.getCall(i);
for (var o = 0; o < options.length; o++) {
var option = options[o];
if (call.args[0].match(option)) {
options.splice(o, 1);
break;
}
}
}
expect(options).to.have.length(0);
});
it('should call the action function', function () {
sinon.spy(program, 'action');
plugin(program);
expect(program.action.calledOnce).to.be(true);
program.action.restore();
});
});
});
});

View file

@ -0,0 +1,147 @@
var root = require('requirefrom')('');
var expect = require('expect.js');
var sinon = require('sinon');
var fs = require('fs');
var rimraf = require('rimraf');
var pluginCleaner = root('src/server/bin/plugin/pluginCleaner');
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('pluginCleaner', function () {
var settings = {
workingPath: 'dummy'
};
describe('cleanPrevious', function () {
var cleaner;
var errorStub;
var logger;
var progress;
var request;
beforeEach(function () {
errorStub = sinon.stub();
logger = pluginLogger(false);
cleaner = pluginCleaner(settings, logger);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
request = {
abort: sinon.stub(),
emit: sinon.stub()
};
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
fs.statSync.restore();
rimraf.sync.restore();
});
it('should resolve if the working path does not exist', function () {
sinon.stub(rimraf, 'sync');
sinon.stub(fs, 'statSync', function () {
var error = new Error('ENOENT');
error.code = 'ENOENT';
throw error;
});
return cleaner.cleanPrevious(logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
});
});
it('should rethrow any exception except ENOENT from fs.statSync', function () {
sinon.stub(rimraf, 'sync');
sinon.stub(fs, 'statSync', function () {
var error = new Error('An Unhandled Error');
throw error;
});
var errorStub = sinon.stub();
return cleaner.cleanPrevious(logger)
.catch(errorStub)
.then(function () {
expect(errorStub.called).to.be(true);
});
});
it('should log a message if there was a working directory', function () {
sinon.stub(rimraf, 'sync');
sinon.stub(fs, 'statSync');
return cleaner.cleanPrevious(logger)
.catch(errorStub)
.then(function (data) {
expect(logger.log.calledWith('Found previous install attempt. Deleting...')).to.be(true);
});
});
it('should rethrow any exception from rimraf.sync', function () {
sinon.stub(fs, 'statSync');
sinon.stub(rimraf, 'sync', function () {
throw new Error('I am an error thrown by rimraf');
});
var errorStub = sinon.stub();
return cleaner.cleanPrevious(logger)
.catch(errorStub)
.then(function () {
expect(errorStub.called).to.be(true);
});
});
it('should resolve if the working path is deleted', function () {
sinon.stub(rimraf, 'sync');
sinon.stub(fs, 'statSync');
return cleaner.cleanPrevious(logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
});
});
});
describe('cleanError', function () {
var cleaner;
var logger;
beforeEach(function () {
logger = pluginLogger(false);
cleaner = pluginCleaner(settings, logger);
});
afterEach(function () {
rimraf.sync.restore();
});
it('should attempt to delete the working directory', function () {
sinon.stub(rimraf, 'sync');
cleaner.cleanError();
expect(rimraf.sync.calledWith(settings.workingPath)).to.be(true);
});
it('should swallow any errors thrown by rimraf.sync', function () {
sinon.stub(rimraf, 'sync', function () {
throw new Error('Something bad happened.');
});
expect(cleaner.cleanError).withArgs(settings).to.not.throwError();
});
});
});
});
});

View file

@ -0,0 +1,249 @@
var root = require('requirefrom')('');
var expect = require('expect.js');
var sinon = require('sinon');
var nock = require('nock');
var glob = require('glob');
var rimraf = require('rimraf');
var join = require('path').join;
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
var pluginDownloader = root('src/server/bin/plugin/pluginDownloader');
describe('kibana cli', function () {
describe('plugin downloader', function () {
var testWorkingPath = join(__dirname, '.test.data');
var logger;
var downloader;
beforeEach(function () {
logger = pluginLogger(false);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(testWorkingPath);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
describe('_downloadSingle', function () {
beforeEach(function () {
downloader = pluginDownloader({}, logger);
});
afterEach(function () {
});
it('should throw an ENOTFOUND error for a 404 error', function () {
var couchdb = nock('http://www.files.com')
.get('/plugin.tar.gz')
.reply(404);
var source = 'http://www.files.com/plugin.tar.gz';
var errorStub = sinon.stub();
return downloader._downloadSingle(source, testWorkingPath, 0, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(errorStub.lastCall.args[0].message).to.match(/ENOTFOUND/);
var files = glob.sync('**/*', { cwd: testWorkingPath });
expect(files).to.eql([]);
});
});
it('should download and extract a valid plugin', function () {
var filename = join(__dirname, 'replies/test-plugin-master.tar.gz');
var couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/plugin.tar.gz')
.replyWithFile(200, filename);
var source = 'http://www.files.com/plugin.tar.gz';
return downloader._downloadSingle(source, testWorkingPath, 0, logger)
.then(function (data) {
var files = glob.sync('**/*', { cwd: testWorkingPath });
var expected = [
'README.md',
'index.js',
'package.json',
'public',
'public/app.js'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it('should abort the download and extraction for a corrupt archive.', function () {
var filename = join(__dirname, 'replies/corrupt.tar.gz');
var couchdb = nock('http://www.files.com')
.get('/plugin.tar.gz')
.replyWithFile(200, filename);
var source = 'http://www.files.com/plugin.tar.gz';
var errorStub = sinon.stub();
return downloader._downloadSingle(source, testWorkingPath, 0, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
var files = glob.sync('**/*', { cwd: testWorkingPath });
expect(files).to.eql([]);
});
});
});
describe('download', function () {
beforeEach(function () {});
afterEach(function () {});
it('should loop through bad urls until it finds a good one.', function () {
var filename = join(__dirname, 'replies/test-plugin-master.tar.gz');
var settings = {
urls: [
'http://www.files.com/badfile1.tar.gz',
'http://www.files.com/badfile2.tar.gz',
'I am a bad uri',
'http://www.files.com/goodfile.tar.gz'
],
workingPath: testWorkingPath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
var couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/goodfile.tar.gz')
.replyWithFile(200, filename);
var errorStub = sinon.stub();
return downloader.download(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.getCall(0).args[0]).to.match(/badfile1.tar.gz/);
expect(logger.log.getCall(1).args[0]).to.match(/badfile2.tar.gz/);
expect(logger.log.getCall(2).args[0]).to.match(/I am a bad uri/);
expect(logger.log.getCall(3).args[0]).to.match(/goodfile.tar.gz/);
expect(logger.log.lastCall.args[0]).to.match(/complete/i);
var files = glob.sync('**/*', { cwd: testWorkingPath });
var expected = [
'README.md',
'index.js',
'package.json',
'public',
'public/app.js'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it('should stop looping through urls when it finds a good one.', function () {
var filename = join(__dirname, 'replies/test-plugin-master.tar.gz');
var settings = {
urls: [
'http://www.files.com/badfile1.tar.gz',
'http://www.files.com/badfile2.tar.gz',
'http://www.files.com/goodfile.tar.gz',
'http://www.files.com/badfile3.tar.gz'
],
workingPath: testWorkingPath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
var couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/goodfile.tar.gz')
.replyWithFile(200, filename)
.get('/badfile3.tar.gz')
.reply(404);
var errorStub = sinon.stub();
return downloader.download(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
for (var i = 0; i < logger.log.callCount; i++) {
expect(logger.log.getCall(i).args[0]).to.not.match(/badfile3.tar.gz/);
}
var files = glob.sync('**/*', { cwd: testWorkingPath });
var expected = [
'README.md',
'index.js',
'package.json',
'public',
'public/app.js'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it('should throw an error when it doesn\'t find a good url.', function () {
var settings = {
urls: [
'http://www.files.com/badfile1.tar.gz',
'http://www.files.com/badfile2.tar.gz',
'http://www.files.com/badfile3.tar.gz'
],
workingPath: testWorkingPath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
var couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/badfile3.tar.gz')
.reply(404);
var errorStub = sinon.stub();
return downloader.download(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(errorStub.lastCall.args[0].message).to.match(/not a valid/i);
var files = glob.sync('**/*', { cwd: testWorkingPath });
expect(files).to.eql([]);
});
});
});
});
});

View file

@ -0,0 +1,74 @@
var root = require('requirefrom')('');
var expect = require('expect.js');
var sinon = require('sinon');
var nock = require('nock');
var glob = require('glob');
var rimraf = require('rimraf');
var fs = require('fs');
var join = require('path').join;
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
var pluginInstaller = root('src/server/bin/plugin/pluginInstaller');
var Promise = require('bluebird');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('pluginInstaller', function () {
var logger;
var testWorkingPath;
var processExitStub;
var statSyncStub;
beforeEach(function () {
processExitStub = undefined;
statSyncStub = undefined;
logger = pluginLogger(false);
testWorkingPath = join(__dirname, '.test.data');
rimraf.sync(testWorkingPath);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
});
afterEach(function () {
if (processExitStub) processExitStub.restore();
if (statSyncStub) statSyncStub.restore();
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
it('should throw an error if the workingPath already exists.', function () {
processExitStub = sinon.stub(process, 'exit');
fs.mkdirSync(testWorkingPath);
var settings = {
pluginPath: testWorkingPath
};
var errorStub = sinon.stub();
return pluginInstaller.install(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(logger.error.firstCall.args[0]).to.match(/already exists/);
expect(process.exit.called).to.be(true);
});
});
it('should rethrow any non "ENOENT" error from fs.', function () {
statSyncStub = sinon.stub(fs, 'statSync', function () {
throw new Error('This is unexpected.');
});
var settings = {
pluginPath: testWorkingPath
};
expect(pluginInstaller.install).withArgs(settings, logger).to.throwException(/this is unexpected/i);
});
});
});
});

View file

@ -0,0 +1,128 @@
var root = require('requirefrom')('');
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
var expect = require('expect.js');
var sinon = require('sinon');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('logger', function () {
var logger;
describe('logger.log', function () {
beforeEach(function () {
sinon.spy(process.stdout, 'write');
});
afterEach(function () {
process.stdout.write.restore();
});
it('should log messages to the console and append a new line', function () {
logger = pluginLogger({ silent: false, quiet: false });
var message = 'this is my message';
logger.log(message);
var callCount = process.stdout.write.callCount;
expect(process.stdout.write.getCall(callCount - 2).args[0]).to.be(message);
expect(process.stdout.write.getCall(callCount - 1).args[0]).to.be('\n');
});
it('should log messages to the console and append not append a new line', function () {
logger = pluginLogger({ silent: false, quiet: false });
for (var i = 0; i < 10; i++) {
logger.log('.', true);
}
logger.log('Done!');
expect(process.stdout.write.callCount).to.be(13);
expect(process.stdout.write.getCall(0).args[0]).to.be('.');
expect(process.stdout.write.getCall(1).args[0]).to.be('.');
expect(process.stdout.write.getCall(2).args[0]).to.be('.');
expect(process.stdout.write.getCall(3).args[0]).to.be('.');
expect(process.stdout.write.getCall(4).args[0]).to.be('.');
expect(process.stdout.write.getCall(5).args[0]).to.be('.');
expect(process.stdout.write.getCall(6).args[0]).to.be('.');
expect(process.stdout.write.getCall(7).args[0]).to.be('.');
expect(process.stdout.write.getCall(8).args[0]).to.be('.');
expect(process.stdout.write.getCall(9).args[0]).to.be('.');
expect(process.stdout.write.getCall(10).args[0]).to.be('\n');
expect(process.stdout.write.getCall(11).args[0]).to.be('Done!');
expect(process.stdout.write.getCall(12).args[0]).to.be('\n');
});
it('should not log any messages when quiet is set', function () {
logger = pluginLogger({ silent: false, quiet: true });
var message = 'this is my message';
logger.log(message);
for (var i = 0; i < 10; i++) {
logger.log('.', true);
}
logger.log('Done!');
expect(process.stdout.write.callCount).to.be(0);
});
it('should not log any messages when silent is set', function () {
logger = pluginLogger({ silent: true, quiet: false });
var message = 'this is my message';
logger.log(message);
for (var i = 0; i < 10; i++) {
logger.log('.', true);
}
logger.log('Done!');
expect(process.stdout.write.callCount).to.be(0);
});
});
describe('logger.error', function () {
beforeEach(function () {
sinon.spy(process.stderr, 'write');
});
afterEach(function () {
process.stderr.write.restore();
});
it('should log error messages to the console and append a new line', function () {
logger = pluginLogger({ silent: false, quiet: false });
var message = 'this is my error';
logger.error(message);
expect(process.stderr.write.calledWith(message + '\n')).to.be(true);
});
it('should log error messages to the console when quiet is set', function () {
logger = pluginLogger({ silent: false, quiet: true });
var message = 'this is my error';
logger.error(message);
expect(process.stderr.write.calledWith(message + '\n')).to.be(true);
});
it('should not log any error messages when silent is set', function () {
logger = pluginLogger({ silent: true, quiet: false });
var message = 'this is my error';
logger.error(message);
expect(process.stderr.write.callCount).to.be(0);
});
});
});
});
});

View file

@ -0,0 +1,302 @@
var root = require('requirefrom')('');
var expect = require('expect.js');
var sinon = require('sinon');
var progressReporter = root('src/server/bin/plugin/progressReporter');
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('progressReporter', function () {
var logger;
var progress;
var request;
beforeEach(function () {
logger = pluginLogger(false);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
request = {
abort: sinon.stub(),
emit: sinon.stub()
};
progress = progressReporter(logger, request);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
});
describe('handleResponse', function () {
describe('bad response codes', function () {
function testErrorResponse(element, index, array) {
it('should set the state to error for response code = ' + element, function () {
progress.handleResponse({ statusCode: element });
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(errorStub.lastCall.args[0].message).to.match(/ENOTFOUND/);
});
});
}
var badCodes = [
'400', '401', '402', '403', '404', '405', '406', '407', '408', '409', '410',
'411', '412', '413', '414', '415', '416', '417', '500', '501', '502', '503',
'504', '505'
];
badCodes.forEach(testErrorResponse);
});
describe('good response codes', function () {
function testSuccessResponse(statusCode, index, array) {
it('should set the state to success for response code = ' + statusCode, function () {
progress.handleResponse({ statusCode: statusCode, headers: { 'content-length': 1000 } });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.getCall(logger.log.callCount - 2).args[0]).to.match(/1000/);
});
});
}
function testUnknownNumber(statusCode, index, array) {
it('should log "unknown number of" for response code = ' + statusCode + ' without content-length header', function () {
progress.handleResponse({ statusCode: statusCode, headers: {} });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.getCall(logger.log.callCount - 2).args[0]).to.match(/unknown number/);
});
});
}
var goodCodes = [
'200', '201', '202', '203', '204', '205', '206', '300', '301', '302', '303',
'304', '305', '306', '307'
];
goodCodes.forEach(testSuccessResponse);
goodCodes.forEach(testUnknownNumber);
});
});
describe('handleData', function () {
it('should do nothing if the reporter is in an error state', function () {
progress.handleResponse({ statusCode: 400 });
progress.handleData({ length: 100 });
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(progress.hasError()).to.be(true);
expect(request.abort.called).to.be(true);
expect(logger.log.callCount).to.be(0);
});
});
it('should do nothing if handleResponse hasn\'t successfully executed yet', function () {
progress.handleData({ length: 100 });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(logger.log.callCount).to.be(1);
expect(logger.log.lastCall.args[0]).to.match(/complete/i);
});
});
it('should do nothing if handleResponse was called without a content-length header', function () {
progress.handleResponse({ statusCode: 200, headers: {} });
progress.handleData({ length: 100 });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(logger.log.callCount).to.be(2);
expect(logger.log.getCall(0).args[0]).to.match(/downloading/i);
expect(logger.log.getCall(1).args[0]).to.match(/complete/i);
});
});
it('should show a max of 20 dots for full prgress', function () {
progress.handleResponse({ statusCode: 200, headers: { 'content-length': 1000 } });
progress.handleData({ length: 1000 });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(logger.log.callCount).to.be(22);
expect(logger.log.getCall(0).args[0]).to.match(/downloading/i);
expect(logger.log.getCall(1).args[0]).to.be('.');
expect(logger.log.getCall(2).args[0]).to.be('.');
expect(logger.log.getCall(3).args[0]).to.be('.');
expect(logger.log.getCall(4).args[0]).to.be('.');
expect(logger.log.getCall(5).args[0]).to.be('.');
expect(logger.log.getCall(6).args[0]).to.be('.');
expect(logger.log.getCall(7).args[0]).to.be('.');
expect(logger.log.getCall(8).args[0]).to.be('.');
expect(logger.log.getCall(9).args[0]).to.be('.');
expect(logger.log.getCall(10).args[0]).to.be('.');
expect(logger.log.getCall(11).args[0]).to.be('.');
expect(logger.log.getCall(12).args[0]).to.be('.');
expect(logger.log.getCall(13).args[0]).to.be('.');
expect(logger.log.getCall(14).args[0]).to.be('.');
expect(logger.log.getCall(15).args[0]).to.be('.');
expect(logger.log.getCall(16).args[0]).to.be('.');
expect(logger.log.getCall(17).args[0]).to.be('.');
expect(logger.log.getCall(18).args[0]).to.be('.');
expect(logger.log.getCall(19).args[0]).to.be('.');
expect(logger.log.getCall(20).args[0]).to.be('.');
expect(logger.log.getCall(21).args[0]).to.match(/complete/i);
});
});
it('should show dot for each 5% of completion', function () {
progress.handleResponse({ statusCode: 200, headers: { 'content-length': 1000 } });
expect(logger.log.callCount).to.be(1);
progress.handleData({ length: 50 }); //5%
expect(logger.log.callCount).to.be(2);
progress.handleData({ length: 100 }); //15%
expect(logger.log.callCount).to.be(4);
progress.handleData({ length: 200 }); //25%
expect(logger.log.callCount).to.be(8);
progress.handleData({ length: 590 }); //94%
expect(logger.log.callCount).to.be(20);
progress.handleData({ length: 60 }); //100%
expect(logger.log.callCount).to.be(21);
//Any progress over 100% should be ignored.
progress.handleData({ length: 9999 });
expect(logger.log.callCount).to.be(21);
progress.handleEnd();
expect(logger.log.callCount).to.be(22);
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.getCall(0).args[0]).to.match(/downloading/i);
expect(logger.log.getCall(21).args[0]).to.match(/complete/i);
});
});
});
describe('handleEnd', function () {
it('should reject the deferred with a ENOTFOUND error if the reporter is in an error state', function () {
progress.handleResponse({ statusCode: 400 });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.firstCall.args[0].message).to.match(/ENOTFOUND/);
expect(errorStub.called).to.be(true);
});
});
it('should resolve if the reporter is not in an error state', function () {
progress.handleResponse({ statusCode: 307, headers: { 'content-length': 1000 } });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.lastCall.args[0]).to.match(/complete/i);
});
});
});
describe('handleError', function () {
it('should log any errors', function () {
progress.handleError('ERRORMESSAGE', new Error('oops!'));
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(logger.error.callCount).to.be(1);
expect(logger.error.lastCall.args[0]).to.match(/oops!/);
});
});
it('should set the error state of the reporter', function () {
progress.handleError('ERRORMESSAGE', new Error('oops!'));
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(progress.hasError()).to.be(true);
});
});
it('should ignore all errors except the first.', function () {
progress.handleError('ERRORMESSAGE', new Error('oops!'));
progress.handleError('ERRORMESSAGE', new Error('second error!'));
progress.handleError('ERRORMESSAGE', new Error('third error!'));
progress.handleError('ERRORMESSAGE', new Error('fourth error!'));
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(logger.error.callCount).to.be(1);
expect(logger.error.lastCall.args[0]).to.match(/oops!/);
});
});
});
});
});
});

Binary file not shown.

View file

@ -0,0 +1,13 @@
{
"name": "test-plugin",
"version": "1.0.0",
"description": "just a test plugin",
"repository": {
"type": "git",
"url": "http://website.git"
},
"dependencies": {
"bluebird": "2.9.30"
},
"license": "Apache-2.0"
}

View file

@ -0,0 +1,323 @@
var root = require('requirefrom')('');
var settingParser = root('src/server/bin/plugin/settingParser');
var path = require('path');
var expect = require('expect.js');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('command line option parsing', function () {
describe('parseMilliseconds function', function () {
var parser = settingParser();
it('should return 0 for an empty string', function () {
var value = '';
var result = parser.parseMilliseconds(value);
expect(result).to.be(0);
});
it('should return 0 for a number with an invalid unit of measure', function () {
var result = parser.parseMilliseconds('1gigablasts');
expect(result).to.be(0);
});
it('should assume a number with no unit of measure is specified as milliseconds', function () {
var result = parser.parseMilliseconds(1);
expect(result).to.be(1);
result = parser.parseMilliseconds('1');
expect(result).to.be(1);
});
it('should interpret a number with "s" as the unit of measure as seconds', function () {
var result = parser.parseMilliseconds('5s');
expect(result).to.be(5 * 1000);
});
it('should interpret a number with "second" as the unit of measure as seconds', function () {
var result = parser.parseMilliseconds('5second');
expect(result).to.be(5 * 1000);
});
it('should interpret a number with "seconds" as the unit of measure as seconds', function () {
var result = parser.parseMilliseconds('5seconds');
expect(result).to.be(5 * 1000);
});
it('should interpret a number with "m" as the unit of measure as minutes', function () {
var result = parser.parseMilliseconds('9m');
expect(result).to.be(9 * 1000 * 60);
});
it('should interpret a number with "minute" as the unit of measure as minutes', function () {
var result = parser.parseMilliseconds('9minute');
expect(result).to.be(9 * 1000 * 60);
});
it('should interpret a number with "minutes" as the unit of measure as minutes', function () {
var result = parser.parseMilliseconds('9minutes');
expect(result).to.be(9 * 1000 * 60);
});
});
describe('parse function', function () {
var options;
var parser;
beforeEach(function () {
options = { install: 'dummy/dummy' };
});
it('should require the user to specify either install and remove', function () {
options.install = null;
parser = settingParser(options);
expect(parser.parse).withArgs().to.throwError(/Please specify either --install or --remove./);
});
it('should not allow the user to specify both install and remove', function () {
options.remove = 'package';
options.install = 'org/package/version';
parser = settingParser(options);
expect(parser.parse).withArgs().to.throwError(/Please specify either --install or --remove./);
});
describe('quiet option', function () {
it('should default to false', function () {
parser = settingParser(options);
var settings = parser.parse(options);
expect(settings.quiet).to.be(false);
});
it('should set settings.quiet property to true', function () {
options.parent = { quiet: true };
parser = settingParser(options);
var settings = parser.parse(options);
expect(settings.quiet).to.be(true);
});
});
describe('silent option', function () {
it('should default to false', function () {
parser = settingParser(options);
var settings = parser.parse(options);
expect(settings).to.have.property('silent', false);
});
it('should set settings.silent property to true', function () {
options.silent = true;
parser = settingParser(options);
var settings = parser.parse(options);
expect(settings).to.have.property('silent', true);
});
});
describe('timeout option', function () {
it('should default to 0 (milliseconds)', function () {
parser = settingParser(options);
var settings = parser.parse(options);
expect(settings).to.have.property('timeout', 0);
});
it('should set settings.timeout property to specified value', function () {
options.timeout = 1234;
parser = settingParser(options);
var settings = parser.parse(options);
expect(settings).to.have.property('timeout', 1234);
});
});
describe('install option', function () {
it('should set settings.action property to "install"', function () {
options.install = 'org/package/version';
parser = settingParser(options);
var settings = parser.parse(options);
expect(settings).to.have.property('action', 'install');
});
it('should allow two parts to the install parameter', function () {
options.install = 'kibana/test-plugin';
parser = settingParser(options);
expect(parser.parse).withArgs().to.not.throwError();
var settings = parser.parse(options);
expect(settings).to.have.property('organization', 'kibana');
expect(settings).to.have.property('package', 'test-plugin');
expect(settings).to.have.property('version', undefined);
});
it('should allow three parts to the install parameter', function () {
options.install = 'kibana/test-plugin/v1.0.1';
parser = settingParser(options);
expect(parser.parse).withArgs().to.not.throwError();
var settings = parser.parse(options);
expect(settings).to.have.property('organization', 'kibana');
expect(settings).to.have.property('package', 'test-plugin');
expect(settings).to.have.property('version', 'v1.0.1');
});
it('should not allow one part to the install parameter', function () {
options.install = 'test-plugin';
parser = settingParser(options);
expect(parser.parse).withArgs().to.throwError(/Invalid install option. Please use the format <org>\/<plugin>\/<version>./);
});
it('should not allow more than three parts to the install parameter', function () {
options.install = 'kibana/test-plugin/v1.0.1/dummy';
parser = settingParser(options);
expect(parser.parse).withArgs().to.throwError(/Invalid install option. Please use the format <org>\/<plugin>\/<version>./);
});
it('should populate the urls collection properly when no version specified', function () {
options.install = 'kibana/test-plugin';
parser = settingParser(options);
var settings = parser.parse();
expect(settings.urls).to.have.property('length', 2);
expect(settings.urls).to.contain('https://download.elastic.co/kibana/test-plugin/test-plugin-latest.tar.gz');
expect(settings.urls).to.contain('https://github.com/kibana/test-plugin/archive/master.tar.gz');
});
it('should populate the urls collection properly version specified', function () {
options.install = 'kibana/test-plugin/v1.1.1';
parser = settingParser(options);
var settings = parser.parse();
expect(settings.urls).to.have.property('length', 2);
expect(settings.urls).to.contain('https://download.elastic.co/kibana/test-plugin/test-plugin-v1.1.1.tar.gz');
expect(settings.urls).to.contain('https://github.com/kibana/test-plugin/archive/v1.1.1.tar.gz');
});
it('should populate the pluginPath', function () {
options.install = 'kibana/test-plugin';
parser = settingParser(options);
var settings = parser.parse();
var expected = path.resolve(__dirname, '..', '..', '..', '..', '..', 'src', 'server', 'bin', 'plugins', 'test-plugin');
expect(settings).to.have.property('pluginPath', expected);
});
describe('with url option', function () {
it('should allow one part to the install parameter', function () {
options.install = 'test-plugin';
options.url = 'http://www.google.com/plugin.tar.gz';
parser = settingParser(options);
expect(parser.parse).withArgs().to.not.throwError();
var settings = parser.parse();
expect(settings).to.have.property('package', 'test-plugin');
});
it('should not allow more than one part to the install parameter', function () {
options.url = 'http://www.google.com/plugin.tar.gz';
options.install = 'kibana/test-plugin';
parser = settingParser(options);
expect(parser.parse).withArgs()
.to.throwError(/Invalid install option. When providing a url, please use the format <plugin>./);
});
it('should result in only the specified url in urls collection', function () {
var url = 'http://www.google.com/plugin.tar.gz';
options.install = 'test-plugin';
options.url = url;
parser = settingParser(options);
var settings = parser.parse();
expect(settings).to.have.property('urls');
expect(settings.urls).to.be.an('array');
expect(settings.urls).to.have.property('length', 1);
expect(settings.urls).to.contain(url);
});
});
});
describe('remove option', function () {
it('should set settings.action property to "remove"', function () {
options.install = null;
options.remove = 'package';
parser = settingParser(options);
var settings = parser.parse();
expect(settings).to.have.property('action', 'remove');
});
it('should allow one part to the remove parameter', function () {
options.install = null;
options.remove = 'test-plugin';
parser = settingParser(options);
var settings = parser.parse();
expect(settings).to.have.property('package', 'test-plugin');
});
it('should not allow more than one part to the install parameter', function () {
options.install = null;
options.remove = 'kibana/test-plugin';
parser = settingParser(options);
expect(parser.parse).withArgs()
.to.throwError(/Invalid remove option. Please use the format <plugin>./);
});
it('should populate the pluginPath', function () {
options.install = null;
options.remove = 'test-plugin';
parser = settingParser(options);
var settings = parser.parse();
var expected = path.resolve(__dirname, '..', '..', '..', '..', '..', 'src', 'server', 'bin', 'plugins', 'test-plugin');
expect(settings).to.have.property('pluginPath', expected);
});
});
});
});
});
});