Merge pull request #6402 from BigFunger/plugin-pack-installer

Plugin pack installer
This commit is contained in:
Jim Unger 2016-03-24 09:22:48 -05:00
commit c649b49ef8
67 changed files with 2121 additions and 1592 deletions

24
bin/kibana-plugin Executable file
View file

@ -0,0 +1,24 @@
#!/bin/sh
SCRIPT=$0
# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
while [ -h "$SCRIPT" ] ; do
ls=$(ls -ld "$SCRIPT")
# Drop everything prior to ->
link=$(expr "$ls" : '.*-> \(.*\)$')
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=$(dirname "$SCRIPT")/"$link"
fi
done
DIR="$(dirname "${SCRIPT}")/.."
NODE="${DIR}/node/bin/node"
test -x "$NODE" || NODE=$(which node)
if [ ! -x "$NODE" ]; then
echo "unable to find usable node.js executable."
exit 1
fi
exec "${NODE}" $NODE_OPTIONS "${DIR}/src/cli_plugin" ${@}

29
bin/kibana-plugin.bat Normal file
View file

@ -0,0 +1,29 @@
@echo off
SETLOCAL
set SCRIPT_DIR=%~dp0
for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI
set NODE=%DIR%\node\node.exe
WHERE /Q node
IF %ERRORLEVEL% EQU 0 (
for /f "delims=" %%i in ('WHERE node') do set SYS_NODE=%%i
)
If Not Exist "%NODE%" (
IF Exist "%SYS_NODE%" (
set "NODE=%SYS_NODE%"
) else (
Echo unable to find usable node.js executable.
Exit /B 1
)
)
TITLE Kibana Server
"%NODE%" %NODE_OPTIONS% "%DIR%\src\cli_plugin" %*
:finally
ENDLOCAL

View file

@ -135,6 +135,7 @@
"style-loader": "0.12.3",
"tar": "2.2.0",
"url-loader": "0.5.6",
"validate-npm-package-name": "2.2.2",
"webpack": "1.12.1",
"webpack-directory-name-as-main": "1.0.0",
"whatwg-fetch": "0.9.0",

View file

@ -1,10 +1,10 @@
import _ from 'lodash';
import pkg from '../utils/package_json';
import Command from './command';
import serveCommand from './serve/serve';
let argv = process.env.kbnWorkerArgv ? JSON.parse(process.env.kbnWorkerArgv) : process.argv.slice();
let program = new Command('bin/kibana');
const argv = process.env.kbnWorkerArgv ? JSON.parse(process.env.kbnWorkerArgv) : process.argv.slice();
const program = new Command('bin/kibana');
program
.version(pkg.version)
@ -14,15 +14,14 @@ program
);
// attach commands
require('./serve/serve')(program);
require('./plugin/plugin')(program);
serveCommand(program);
program
.command('help <command>')
.description('Get the help for a specific command')
.action(function (cmdName) {
var cmd = _.find(program.commands, { _name: cmdName });
if (!cmd) return this.error(`unknown command ${cmdName}`);
const cmd = _.find(program.commands, { _name: cmdName });
if (!cmd) return program.error(`unknown command ${cmdName}`);
cmd.help();
});
@ -33,7 +32,7 @@ program
});
// check for no command name
var subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//);
const subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//);
if (!subCommand) {
if (_.intersection(argv.slice(2), ['-h', '--help']).length) {

View file

@ -1,31 +0,0 @@
import expect from 'expect.js';
import fileType, { ZIP, TAR } from '../file_type';
describe('kibana cli', function () {
describe('file_type', function () {
it('returns ZIP for .zip filename', function () {
const type = fileType('wat.zip');
expect(type).to.equal(ZIP);
});
it('returns TAR for .tar.gz filename', function () {
const type = fileType('wat.tar.gz');
expect(type).to.equal(TAR);
});
it('returns TAR for .tgz filename', function () {
const type = fileType('wat.tgz');
expect(type).to.equal(TAR);
});
it('returns undefined for unknown file type', function () {
const type = fileType('wat.unknown');
expect(type).to.equal(undefined);
});
it('accepts paths', function () {
const type = fileType('/some/path/to/wat.zip');
expect(type).to.equal(ZIP);
});
it('accepts urls', function () {
const type = fileType('http://example.com/wat.zip');
expect(type).to.equal(ZIP);
});
});
});

View file

@ -1,335 +0,0 @@
import expect from 'expect.js';
import sinon from 'sinon';
import nock from 'nock';
import glob from 'glob-all';
import rimraf from 'rimraf';
import mkdirp from 'mkdirp';
import pluginLogger from '../plugin_logger';
import pluginDownloader from '../plugin_downloader';
import { join } from 'path';
describe('kibana cli', function () {
describe('plugin downloader', function () {
const testWorkingPath = join(__dirname, '.test.data');
const tempArchiveFilePath = join(testWorkingPath, 'archive.part');
let logger;
let downloader;
function expectWorkingPathEmpty() {
const files = glob.sync('**/*', { cwd: testWorkingPath });
expect(files).to.eql([]);
}
function expectWorkingPathNotEmpty() {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'archive.part'
];
expect(files.sort()).to.eql(expected.sort());
}
function shouldReject() {
throw new Error('expected the promise to reject');
}
beforeEach(function () {
logger = pluginLogger(false);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(testWorkingPath);
mkdirp.sync(testWorkingPath);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
describe('_downloadSingle', function () {
beforeEach(function () {
const settings = {
urls: [],
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
});
describe('http downloader', function () {
it('should download an unsupported file type, but return undefined for archiveType', function () {
const filePath = join(__dirname, 'replies/banana.jpg');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10',
'content-type': 'image/jpeg'
})
.get('/banana.jpg')
.replyWithFile(200, filePath);
const sourceUrl = 'http://www.files.com/banana.jpg';
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be(undefined);
expectWorkingPathNotEmpty();
});
});
it('should throw an ENOTFOUND error for a http ulr that returns 404', function () {
const couchdb = nock('http://www.files.com')
.get('/plugin.tar.gz')
.reply(404);
const sourceUrl = 'http://www.files.com/plugin.tar.gz';
return downloader._downloadSingle(sourceUrl)
.then(shouldReject, function (err) {
expect(err.message).to.match(/ENOTFOUND/);
expectWorkingPathEmpty();
});
});
it('should throw an ENOTFOUND error for an invalid url', function () {
const sourceUrl = 'i am an invalid url';
return downloader._downloadSingle(sourceUrl)
.then(shouldReject, function (err) {
expect(err.message).to.match(/ENOTFOUND/);
expectWorkingPathEmpty();
});
});
it('should download a tarball from a valid http url', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10',
'content-type': 'application/x-gzip'
})
.get('/plugin.tar.gz')
.replyWithFile(200, filePath);
const sourceUrl = 'http://www.files.com/plugin.tar.gz';
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.tar.gz');
expectWorkingPathNotEmpty();
});
});
it('should consider .tgz files as archive type .tar.gz', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/plugin.tgz')
.replyWithFile(200, filePath);
const sourceUrl = 'http://www.files.com/plugin.tgz';
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.tar.gz');
expectWorkingPathNotEmpty();
});
});
it('should download a zip from a valid http url', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.zip');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '341965',
'content-type': 'application/zip'
})
.get('/plugin.zip')
.replyWithFile(200, filePath);
const sourceUrl = 'http://www.files.com/plugin.zip';
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.zip');
expectWorkingPathNotEmpty();
});
});
});
describe('local file downloader', function () {
it('should copy an unsupported file type, but return undefined for archiveType', function () {
const filePath = join(__dirname, 'replies/banana.jpg');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10',
'content-type': 'image/jpeg'
})
.get('/banana.jpg')
.replyWithFile(200, filePath);
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be(undefined);
expectWorkingPathNotEmpty();
});
});
it('should throw an ENOTFOUND error for an invalid local file', function () {
const filePath = join(__dirname, 'replies/i-am-not-there.tar.gz');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return downloader._downloadSingle(sourceUrl)
.then(shouldReject, function (err) {
expect(err.message).to.match(/ENOTFOUND/);
expectWorkingPathEmpty();
});
});
it('should copy a tarball from a valid local file', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.tar.gz');
expectWorkingPathNotEmpty();
});
});
it('should copy a zip from a valid local file', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.zip');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.zip');
expectWorkingPathNotEmpty();
});
});
});
});
describe('download', function () {
it('should loop through bad urls until it finds a good one.', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const 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,
tempArchiveFile: tempArchiveFilePath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
const 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, filePath);
return downloader.download(settings, logger)
.then(function (data) {
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/);
expectWorkingPathNotEmpty();
});
});
it('should stop looping through urls when it finds a good one.', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const 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,
tempArchiveFile: tempArchiveFilePath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
const 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, filePath)
.get('/badfile3.tar.gz')
.reply(404);
return downloader.download(settings, logger)
.then(function (data) {
for (let i = 0; i < logger.log.callCount; i++) {
expect(logger.log.getCall(i).args[0]).to.not.match(/badfile3.tar.gz/);
}
expectWorkingPathNotEmpty();
});
});
it('should throw an error when it doesn\'t find a good url.', function () {
const 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,
tempArchiveFile: tempArchiveFilePath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
const 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);
return downloader.download(settings, logger)
.then(shouldReject, function (err) {
expect(err.message).to.match(/no valid url specified/i);
expectWorkingPathEmpty();
});
});
});
});
});

View file

@ -1,131 +0,0 @@
import expect from 'expect.js';
import sinon from 'sinon';
import glob from 'glob-all';
import rimraf from 'rimraf';
import mkdirp from 'mkdirp';
import pluginLogger from '../plugin_logger';
import extract from '../plugin_extractor';
import pluginDownloader from '../plugin_downloader';
import { join } from 'path';
describe('kibana cli', function () {
describe('plugin extractor', function () {
const testWorkingPath = join(__dirname, '.test.data');
const tempArchiveFilePath = join(testWorkingPath, 'archive.part');
let logger;
let downloader;
const settings = {
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath
};
function shouldReject() {
throw new Error('expected the promise to reject');
}
beforeEach(function () {
logger = pluginLogger(false);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(testWorkingPath);
mkdirp.sync(testWorkingPath);
downloader = pluginDownloader(settings, logger);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
function copyReplyFile(filename) {
const filePath = join(__dirname, 'replies', filename);
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return downloader._downloadSingle(sourceUrl);
}
function shouldReject() {
throw new Error('expected the promise to reject');
}
describe('extractArchive', function () {
it('successfully extract a valid tarball', function () {
return copyReplyFile('test_plugin_master.tar.gz')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(() => {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'archive.part',
'README.md',
'index.js',
'package.json',
'public',
'public/app.js'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it('successfully extract a valid zip', function () {
return copyReplyFile('test_plugin_master.zip')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(() => {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'archive.part',
'README.md',
'index.js',
'package.json',
'public',
'public/app.js',
'extra file only in zip.txt'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it('throw an error when extracting a corrupt zip', function () {
return copyReplyFile('corrupt.zip')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(shouldReject, (err) => {
expect(err.message).to.match(/error extracting/i);
});
});
it('throw an error when extracting a corrupt tarball', function () {
return copyReplyFile('corrupt.tar.gz')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(shouldReject, (err) => {
expect(err.message).to.match(/error extracting/i);
});
});
it('throw an error when passed an unknown archive type', function () {
return copyReplyFile('banana.jpg')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(shouldReject, (err) => {
expect(err.message).to.match(/unsupported archive format/i);
});
});
});
});
});

View file

@ -1,55 +0,0 @@
import expect from 'expect.js';
import sinon from 'sinon';
import rimraf from 'rimraf';
import pluginLogger from '../plugin_logger';
import pluginInstaller from '../plugin_installer';
import { mkdirSync } from 'fs';
import { join } from 'path';
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('pluginInstaller', function () {
let logger;
let testWorkingPath;
let processExitStub;
beforeEach(function () {
processExitStub = 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();
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');
mkdirSync(testWorkingPath);
let 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);
});
});
});
});
});

View file

@ -1,382 +0,0 @@
import path from 'path';
import expect from 'expect.js';
import fromRoot from '../../../utils/from_root';
import settingParser from '../setting_parser';
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', pluginDir: fromRoot('installedPlugins') };
});
it('should require the user to specify either install, remove, or list', function () {
options.install = null;
parser = settingParser(options);
expect(parser.parse).withArgs().to.throwError(/Please specify either --install, --remove, or --list./);
});
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, --remove, or --list./);
});
it('should not allow the user to specify both install and list', function () {
options.list = true;
options.install = 'org/package/version';
parser = settingParser(options);
expect(parser.parse).withArgs().to.throwError(/Please specify either --install, --remove, or --list./);
});
it('should not allow the user to specify both remove and list', function () {
options.list = true;
options.remove = 'package';
parser = settingParser(options);
expect(parser.parse).withArgs().to.throwError(/Please specify either --install, --remove, or --list./);
});
it('should not allow the user to specify install, remove, and list', function () {
options.list = true;
options.install = 'org/package/version';
options.remove = 'package';
parser = settingParser(options);
expect(parser.parse).withArgs().to.throwError(/Please specify either --install, --remove, or --list./);
});
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', 1);
expect(settings.urls).to.contain('https://download.elastic.co/kibana/test-plugin/test-plugin-latest.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', 1);
expect(settings.urls).to.contain('https://download.elastic.co/kibana/test-plugin/test-plugin-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 = fromRoot('installedPlugins/test-plugin');
expect(settings).to.have.property('pluginPath', expected);
});
it('should populate the workingPath', function () {
options.install = 'kibana/test-plugin';
parser = settingParser(options);
var settings = parser.parse();
var expected = fromRoot('installedPlugins/.plugin.installing');
expect(settings).to.have.property('workingPath', expected);
});
it('should populate the tempArchiveFile', function () {
options.install = 'kibana/test-plugin';
parser = settingParser(options);
var settings = parser.parse();
var expected = fromRoot('installedPlugins/.plugin.installing/archive.part');
expect(settings).to.have.property('tempArchiveFile', 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 () {
delete options.install;
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 () {
delete options.install;
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 remove parameter', function () {
delete options.install;
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 () {
delete options.install;
options.remove = 'test-plugin';
parser = settingParser(options);
var settings = parser.parse();
var expected = fromRoot('installedPlugins/test-plugin');
expect(settings).to.have.property('pluginPath', expected);
});
});
describe('list option', function () {
it('should set settings.action property to "list"', function () {
delete options.install;
delete options.remove;
options.list = true;
parser = settingParser(options);
var settings = parser.parse();
expect(settings).to.have.property('action', 'list');
});
});
});
});
});
});

View file

@ -1,34 +0,0 @@
import zlib from 'zlib';
import fs from 'fs';
import tar from 'tar';
async function extractArchive(settings) {
await new Promise((resolve, reject) => {
const gunzip = zlib.createGunzip();
const tarExtract = new tar.Extract({ path: settings.workingPath, strip: 1 });
const readStream = fs.createReadStream(settings.tempArchiveFile);
readStream.on('error', reject);
gunzip.on('error', reject);
tarExtract.on('error', reject);
readStream
.pipe(gunzip)
.pipe(tarExtract);
tarExtract.on('finish', resolve);
});
}
export default async function extractTarball(settings, logger) {
try {
logger.log('Extracting plugin archive');
await extractArchive(settings);
logger.log('Extraction complete');
} catch (err) {
logger.error(err);
throw new Error('Error extracting plugin archive');
}
};

View file

@ -1,32 +0,0 @@
import DecompressZip from '@bigfunger/decompress-zip';
async function extractArchive(settings) {
await new Promise((resolve, reject) => {
const unzipper = new DecompressZip(settings.tempArchiveFile);
unzipper.on('error', reject);
unzipper.extract({
path: settings.workingPath,
strip: 1,
filter(file) {
return file.type !== 'SymbolicLink';
}
});
unzipper.on('extract', resolve);
});
}
export default async function extractZip(settings, logger) {
try {
logger.log('Extracting plugin archive');
await extractArchive(settings);
logger.log('Extraction complete');
} catch (err) {
logger.error(err);
throw new Error('Error extracting plugin archive');
}
};

View file

@ -1,14 +0,0 @@
export const TAR = '.tar.gz';
export const ZIP = '.zip';
export default function fileType(filename) {
if (/\.zip$/i.test(filename)) {
return ZIP;
}
if (/\.tar\.gz$/i.test(filename)) {
return TAR;
}
if (/\.tgz$/i.test(filename)) {
return TAR;
}
}

View file

@ -1,75 +0,0 @@
import fromRoot from '../../utils/from_root';
import settingParser from './setting_parser';
import installer from './plugin_installer';
import remover from './plugin_remover';
import lister from './plugin_lister';
import pluginLogger from './plugin_logger';
export default function pluginCli(program) {
function processCommand(command, options) {
let settings;
try {
settings = settingParser(command).parse();
} catch (ex) {
//The logger has not yet been initialized.
console.error(ex.message);
process.exit(64); // eslint-disable-line no-process-exit
}
const logger = pluginLogger(settings);
switch (settings.action) {
case 'install':
installer.install(settings, logger);
break;
case 'remove':
remover.remove(settings, logger);
break;
case 'list':
lister.list(settings, logger);
break;
}
}
program
.command('plugin')
.option('-i, --install <org>/<plugin>/<version>', 'The plugin to install')
.option('-r, --remove <plugin>', 'The plugin to remove')
.option('-l, --list', 'List installed plugins')
.option('-q, --quiet', 'Disable all process messaging except errors')
.option('-s, --silent', 'Disable all process messaging')
.option('-u, --url <url>', 'Specify download url')
.option(
'-c, --config <path>',
'Path to the config file',
fromRoot('config/kibana.yml')
)
.option(
'-t, --timeout <duration>',
'Length of time before failing; 0 for never fail',
settingParser.parseMilliseconds
)
.option(
'-d, --plugin-dir <path>',
'The path to the directory where plugins are stored',
fromRoot('installedPlugins')
)
.description(
'Maintain Plugins',
`
Common examples:
-i username/sample
attempts to download the latest version from the following url:
https://download.elastic.co/username/sample/sample-latest.tar.gz
-i username/sample/v1.1.1
attempts to download version v1.1.1 from the following url:
https://download.elastic.co/username/sample/sample-v1.1.1.tar.gz
-i sample -u http://www.example.com/other_name.tar.gz
attempts to download from the specified url,
and installs the plugin found at that url as "sample"
`
)
.action(processCommand);
};

View file

@ -1,39 +0,0 @@
import rimraf from 'rimraf';
import fs from 'fs';
export default function createPluginCleaner(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);
rimraf.sync(settings.pluginPath);
}
catch (e) {} // eslint-disable-line no-empty
}
return {
cleanPrevious: cleanPrevious,
cleanError: cleanError
};
};

View file

@ -1,51 +0,0 @@
import _ from 'lodash';
import downloadHttpFile from './downloaders/http';
import downloadLocalFile from './downloaders/file';
import { parse as urlParse } from 'url';
export default function createPluginDownloader(settings, logger) {
let archiveType;
let sourceType;
//Attempts to download each url in turn until one is successful
function download() {
const urls = settings.urls.slice(0);
function tryNext() {
const sourceUrl = urls.shift();
if (!sourceUrl) {
throw new Error('No valid url specified.');
}
logger.log(`Attempting to transfer from ${sourceUrl}`);
return downloadSingle(sourceUrl)
.catch((err) => {
if (err.message === 'ENOTFOUND') {
return tryNext();
}
throw (err);
});
}
return tryNext();
}
function downloadSingle(sourceUrl) {
const urlInfo = urlParse(sourceUrl);
let downloadPromise;
if (/^file/.test(urlInfo.protocol)) {
downloadPromise = downloadLocalFile(logger, urlInfo.path, settings.tempArchiveFile);
} else {
downloadPromise = downloadHttpFile(logger, sourceUrl, settings.tempArchiveFile, settings.timeout);
}
return downloadPromise;
}
return {
download: download,
_downloadSingle: downloadSingle
};
};

View file

@ -1,16 +0,0 @@
import zipExtract from './extractors/zip';
import tarGzExtract from './extractors/tar_gz';
import { ZIP, TAR } from './file_type';
export default function extractArchive(settings, logger, archiveType) {
switch (archiveType) {
case ZIP:
return zipExtract(settings, logger);
break;
case TAR:
return tarGzExtract(settings, logger);
break;
default:
throw new Error('Unsupported archive format.');
}
};

View file

@ -1,86 +0,0 @@
import _ from 'lodash';
import fromRoot from '../../utils/from_root';
import pluginDownloader from './plugin_downloader';
import pluginCleaner from './plugin_cleaner';
import pluginExtractor from './plugin_extractor';
import KbnServer from '../../server/kbn_server';
import readYamlConfig from '../serve/read_yaml_config';
import Promise from 'bluebird';
import { sync as rimrafSync } from 'rimraf';
import { statSync, renameSync } from 'fs';
const mkdirp = Promise.promisify(require('mkdirp'));
export default {
install: install
};
function checkForExistingInstall(settings, logger) {
try {
statSync(settings.pluginPath);
logger.error(`Plugin ${settings.package} already exists, please remove before installing a new version`);
process.exit(70); // eslint-disable-line no-process-exit
} catch (e) {
if (e.code !== 'ENOENT') throw e;
}
}
async function rebuildKibanaCache(settings, logger) {
logger.log('Optimizing and caching browser bundles...');
const serverConfig = _.merge(
readYamlConfig(settings.config),
{
env: 'production',
logging: {
silent: settings.silent,
quiet: !settings.silent,
verbose: false
},
optimize: {
useBundleCache: false
},
server: {
autoListen: false
},
plugins: {
initialize: false,
scanDirs: [settings.pluginDir, fromRoot('src/plugins')]
}
}
);
const kbnServer = new KbnServer(serverConfig);
await kbnServer.ready();
await kbnServer.close();
}
async function install(settings, logger) {
logger.log(`Installing ${settings.package}`);
const cleaner = pluginCleaner(settings, logger);
try {
checkForExistingInstall(settings, logger);
await cleaner.cleanPrevious();
await mkdirp(settings.workingPath);
const downloader = pluginDownloader(settings, logger);
const { archiveType } = await downloader.download();
await pluginExtractor (settings, logger, archiveType);
rimrafSync(settings.tempArchiveFile);
renameSync(settings.workingPath, settings.pluginPath);
await rebuildKibanaCache(settings, logger);
logger.log('Plugin installation complete');
} catch (err) {
logger.error(`Plugin installation was unsuccessful due to error "${err.message}"`);
cleaner.cleanError();
process.exit(70); // eslint-disable-line no-process-exit
}
}

View file

@ -1,8 +0,0 @@
import fs from 'fs';
export function list(settings, logger) {
fs.readdirSync(settings.pluginDir)
.forEach(function (pluginFile) {
logger.log(pluginFile);
});
}

View file

@ -1,44 +0,0 @@
export default function createPluginLogger(settings) {
let previousLineEnded = true;
const silent = !!settings.silent;
const 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

@ -1,23 +0,0 @@
import fs from 'fs';
import rimraf from '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) {
logger.error(`Unable to remove plugin "${settings.package}" because of error: "${err.message}"`);
process.exit(74); // eslint-disable-line no-process-exit
}
}

View file

@ -1,38 +0,0 @@
/*
Generates file transfer progress messages
*/
export default function createProgressReporter(logger) {
let dotCount = 0;
let runningTotal = 0;
let totalSize = 0;
function init(size) {
totalSize = size;
let totalDesc = totalSize || 'unknown number of';
logger.log(`Transferring ${totalDesc} bytes`, true);
}
//Should log a dot for every 5% of progress
function progress(size) {
if (!totalSize) return;
runningTotal += size;
let newDotCount = Math.round(runningTotal / totalSize * 100 / 5);
if (newDotCount > 20) newDotCount = 20;
for (let i = 0; i < (newDotCount - dotCount); i++) {
logger.log('.', true);
}
dotCount = newDotCount;
}
function complete() {
logger.log(`Transfer complete`, false);
}
return {
init: init,
progress: progress,
complete: complete
};
};

View file

@ -1,114 +0,0 @@
import expiry from 'expiry-js';
import { intersection } from 'lodash';
import { resolve } from 'path';
export default function createSettingParser(options) {
function parseMilliseconds(val) {
let result;
try {
let timeVal = expiry(val);
result = timeVal.asMilliseconds();
} catch (ex) {
result = 0;
}
return result;
}
function generateDownloadUrl(settings) {
const version = (settings.version) || 'latest';
const filename = settings.package + '-' + version + '.tar.gz';
return 'https://download.elastic.co/' + settings.organization + '/' + settings.package + '/' + filename;
}
function areMultipleOptionsChosen(options, choices) {
return intersection(Object.keys(options), choices).length > 1;
}
function parse() {
let parts;
let settings = {
timeout: 0,
silent: false,
quiet: false,
urls: []
};
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.config) {
settings.config = options.config;
}
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));
}
}
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 (options.list) {
settings.action = 'list';
}
if (!settings.action || areMultipleOptionsChosen(options, [ 'install', 'remove', 'list' ])) {
throw new Error('Please specify either --install, --remove, or --list.');
}
settings.pluginDir = options.pluginDir;
if (settings.package) {
settings.pluginPath = resolve(settings.pluginDir, settings.package);
settings.workingPath = resolve(settings.pluginDir, '.plugin.installing');
settings.tempArchiveFile = resolve(settings.workingPath, 'archive.part');
}
return settings;
}
return {
parse: parse,
parseMilliseconds: parseMilliseconds
};
};

43
src/cli_plugin/cli.js Normal file
View file

@ -0,0 +1,43 @@
import _ from 'lodash';
import pkg from '../utils/package_json';
import Command from '../cli/command';
import listCommand from './list';
import installCommand from './install';
import removeCommand from './remove';
let argv = process.env.kbnWorkerArgv ? JSON.parse(process.env.kbnWorkerArgv) : process.argv.slice();
let program = new Command('bin/kibana-plugin');
program
.version(pkg.version)
.description(
'The Kibana plugin manager enables you to install and remove plugins that ' +
'provide additional functionality to Kibana'
);
listCommand(program);
installCommand(program);
removeCommand(program);
program
.command('help <command>')
.description('get the help for a specific command')
.action(function (cmdName) {
var cmd = _.find(program.commands, { _name: cmdName });
if (!cmd) return program.error(`unknown command ${cmdName}`);
cmd.help();
});
program
.command('*', null, { noHelp: true })
.action(function (cmd, options) {
program.error(`unknown command ${cmd}`);
});
// check for no command name
var subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//);
if (!subCommand) {
program.defaultHelp();
}
program.parse(argv);

5
src/cli_plugin/index.js Normal file
View file

@ -0,0 +1,5 @@
// load the babel options seperately so that they can modify the process.env
// before calling babel/register
const babelOptions = require('../optimize/babel_options').node;
require('babel/register')(babelOptions);
require('./cli');

View file

@ -3,8 +3,8 @@ import sinon from 'sinon';
import fs from 'fs';
import rimraf from 'rimraf';
import pluginCleaner from '../plugin_cleaner';
import pluginLogger from '../plugin_logger';
import { cleanPrevious, cleanArtifacts } from '../cleanup';
import Logger from '../../lib/logger';
describe('kibana cli', function () {
@ -24,8 +24,7 @@ describe('kibana cli', function () {
beforeEach(function () {
errorStub = sinon.stub();
logger = pluginLogger(false);
cleaner = pluginCleaner(settings, logger);
logger = new Logger(settings);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
request = {
@ -49,7 +48,7 @@ describe('kibana cli', function () {
throw error;
});
return cleaner.cleanPrevious(logger)
return cleanPrevious(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
@ -64,7 +63,7 @@ describe('kibana cli', function () {
});
errorStub = sinon.stub();
return cleaner.cleanPrevious(logger)
return cleanPrevious(settings, logger)
.catch(errorStub)
.then(function () {
expect(errorStub.called).to.be(true);
@ -75,7 +74,7 @@ describe('kibana cli', function () {
sinon.stub(rimraf, 'sync');
sinon.stub(fs, 'statSync');
return cleaner.cleanPrevious(logger)
return cleanPrevious(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(logger.log.calledWith('Found previous install attempt. Deleting...')).to.be(true);
@ -89,7 +88,7 @@ describe('kibana cli', function () {
});
errorStub = sinon.stub();
return cleaner.cleanPrevious(logger)
return cleanPrevious(settings, logger)
.catch(errorStub)
.then(function () {
expect(errorStub.called).to.be(true);
@ -100,7 +99,7 @@ describe('kibana cli', function () {
sinon.stub(rimraf, 'sync');
sinon.stub(fs, 'statSync');
return cleaner.cleanPrevious(logger)
return cleanPrevious(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
@ -109,13 +108,11 @@ describe('kibana cli', function () {
});
describe('cleanError', function () {
let cleaner;
describe('cleanArtifacts', function () {
let logger;
beforeEach(function () {
logger = pluginLogger(false);
cleaner = pluginCleaner(settings, logger);
logger = new Logger(settings);
});
afterEach(function () {
@ -125,7 +122,7 @@ describe('kibana cli', function () {
it('should attempt to delete the working directory', function () {
sinon.stub(rimraf, 'sync');
cleaner.cleanError();
cleanArtifacts(settings);
expect(rimraf.sync.calledWith(settings.workingPath)).to.be(true);
});
@ -134,7 +131,7 @@ describe('kibana cli', function () {
throw new Error('Something bad happened.');
});
expect(cleaner.cleanError).withArgs(settings).to.not.throwError();
expect(cleanArtifacts).withArgs(settings).to.not.throwError();
});
});

View file

@ -0,0 +1,226 @@
import expect from 'expect.js';
import sinon from 'sinon';
import nock from 'nock';
import glob from 'glob-all';
import rimraf from 'rimraf';
import mkdirp from 'mkdirp';
import Logger from '../../lib/logger';
import { download, _downloadSingle } from '../download';
import { join } from 'path';
describe('kibana cli', function () {
describe('plugin downloader', function () {
const testWorkingPath = join(__dirname, '.test.data');
const tempArchiveFilePath = join(testWorkingPath, 'archive.part');
const settings = {
urls: [],
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
timeout: 0
};
const logger = new Logger(settings);
function expectWorkingPathEmpty() {
const files = glob.sync('**/*', { cwd: testWorkingPath });
expect(files).to.eql([]);
}
function expectWorkingPathNotEmpty() {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'archive.part'
];
expect(files.sort()).to.eql(expected.sort());
}
function shouldReject() {
throw new Error('expected the promise to reject');
}
beforeEach(function () {
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(testWorkingPath);
mkdirp.sync(testWorkingPath);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
describe('_downloadSingle', function () {
beforeEach(function () {
});
describe('http downloader', function () {
it('should throw an ENOTFOUND error for a http ulr that returns 404', function () {
const couchdb = nock('http://example.com')
.get('/plugin.tar.gz')
.reply(404);
const sourceUrl = 'http://example.com/plugin.tar.gz';
return _downloadSingle(settings, logger, sourceUrl)
.then(shouldReject, function (err) {
expect(err.message).to.match(/ENOTFOUND/);
expectWorkingPathEmpty();
});
});
it('should throw an ENOTFOUND error for an invalid url', function () {
const sourceUrl = 'i am an invalid url';
return _downloadSingle(settings, logger, sourceUrl)
.then(shouldReject, function (err) {
expect(err.message).to.match(/ENOTFOUND/);
expectWorkingPathEmpty();
});
});
it('should download a file from a valid http url', function () {
const filePath = join(__dirname, 'replies/banana.jpg');
const couchdb = nock('http://example.com')
.defaultReplyHeaders({
'content-length': '341965',
'content-type': 'application/zip'
})
.get('/plugin.zip')
.replyWithFile(200, filePath);
const sourceUrl = 'http://example.com/plugin.zip';
return _downloadSingle(settings, logger, sourceUrl)
.then(function () {
expectWorkingPathNotEmpty();
});
});
});
describe('local file downloader', function () {
it('should throw an ENOTFOUND error for an invalid local file', function () {
const filePath = join(__dirname, 'replies/i-am-not-there.zip');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return _downloadSingle(settings, logger, sourceUrl)
.then(shouldReject, function (err) {
expect(err.message).to.match(/ENOTFOUND/);
expectWorkingPathEmpty();
});
});
it('should copy a valid local file', function () {
const filePath = join(__dirname, 'replies/banana.jpg');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return _downloadSingle(settings, logger, sourceUrl)
.then(function () {
expectWorkingPathNotEmpty();
});
});
});
});
describe('download', function () {
it('should loop through bad urls until it finds a good one.', function () {
const filePath = join(__dirname, 'replies/test_plugin.zip');
settings.urls = [
'http://example.com/badfile1.tar.gz',
'http://example.com/badfile2.tar.gz',
'I am a bad uri',
'http://example.com/goodfile.tar.gz'
];
const couchdb = nock('http://example.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/goodfile.tar.gz')
.replyWithFile(200, filePath);
return download(settings, logger)
.then(function () {
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/);
expectWorkingPathNotEmpty();
});
});
it('should stop looping through urls when it finds a good one.', function () {
const filePath = join(__dirname, 'replies/test_plugin.zip');
settings.urls = [
'http://example.com/badfile1.tar.gz',
'http://example.com/badfile2.tar.gz',
'http://example.com/goodfile.tar.gz',
'http://example.com/badfile3.tar.gz'
];
const couchdb = nock('http://example.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/goodfile.tar.gz')
.replyWithFile(200, filePath)
.get('/badfile3.tar.gz')
.reply(404);
return download(settings, logger)
.then(function () {
for (let i = 0; i < logger.log.callCount; i++) {
expect(logger.log.getCall(i).args[0]).to.not.match(/badfile3.tar.gz/);
}
expectWorkingPathNotEmpty();
});
});
it('should throw an error when it doesn\'t find a good url.', function () {
settings.urls = [
'http://example.com/badfile1.tar.gz',
'http://example.com/badfile2.tar.gz',
'http://example.com/badfile3.tar.gz'
];
const couchdb = nock('http://example.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/badfile3.tar.gz')
.reply(404);
return download(settings, logger)
.then(shouldReject, function (err) {
expect(err.message).to.match(/no valid url specified/i);
expectWorkingPathEmpty();
});
});
});
});
});

View file

@ -1,6 +1,6 @@
import expect from 'expect.js';
import sinon from 'sinon';
import plugin from '../plugin';
import index from '../index';
describe('kibana cli', function () {
@ -18,8 +18,8 @@ describe('kibana cli', function () {
it('should define the command', function () {
sinon.spy(program, 'command');
plugin(program);
expect(program.command.calledWith('plugin')).to.be(true);
index(program);
expect(program.command.calledWith('install <plugin/url>')).to.be(true);
program.command.restore();
});
@ -27,8 +27,8 @@ describe('kibana cli', function () {
it('should define the description', function () {
sinon.spy(program, 'description');
plugin(program);
expect(program.description.calledWith('Maintain Plugins')).to.be(true);
index(program);
expect(program.description.calledWith('install a plugin')).to.be(true);
program.description.restore();
});
@ -37,14 +37,14 @@ describe('kibana cli', function () {
const spy = sinon.spy(program, 'option');
const options = [
/-i/,
/-r/,
/-q/,
/-s/,
/-u/,
/-t/
/-c/,
/-t/,
/-d/
];
plugin(program);
index(program);
for (let i = 0; i < spy.callCount; i++) {
const call = spy.getCall(i);
@ -63,7 +63,7 @@ describe('kibana cli', function () {
it('should call the action function', function () {
sinon.spy(program, 'action');
plugin(program);
index(program);
expect(program.action.calledOnce).to.be(true);
program.action.restore();

View file

@ -0,0 +1,170 @@
import expect from 'expect.js';
import sinon from 'sinon';
import Logger from '../../lib/logger';
import { join } from 'path';
import rimraf from 'rimraf';
import mkdirp from 'mkdirp';
import { existingInstall, assertVersion } from '../kibana';
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('kibana', function () {
const testWorkingPath = join(__dirname, '.test.data');
const tempArchiveFilePath = join(testWorkingPath, 'archive.part');
const settings = {
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
plugin: 'test-plugin',
version: '1.0.0',
plugins: [ { name: 'foo', path: join(testWorkingPath, 'foo') } ]
};
const logger = new Logger(settings);
describe('assertVersion', function () {
beforeEach(function () {
rimraf.sync(testWorkingPath);
mkdirp.sync(testWorkingPath);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
it('should succeed with exact match', function () {
const settings = {
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
plugin: 'test-plugin',
version: '5.0.0-snapshot',
plugins: [ { name: 'foo', path: join(testWorkingPath, 'foo'), version: '5.0.0-snapshot' } ]
};
const errorStub = sinon.stub();
try {
assertVersion(settings);
}
catch (err) {
errorStub(err);
}
expect(errorStub.called).to.be(false);
});
it('should throw an error if plugin does contain a version.', function () {
const errorStub = sinon.stub();
try {
assertVersion(settings);
}
catch (err) {
errorStub(err);
}
expect(errorStub.firstCall.args[0]).to.match(/plugin version not found/i);
});
it('should throw an error if plugin version does does not match kibana version', function () {
const errorStub = sinon.stub();
settings.plugins[0].version = '1.2.3.4';
try {
assertVersion(settings);
}
catch (err) {
errorStub(err);
}
expect(errorStub.firstCall.args[0]).to.match(/incorrect version/i);
});
it('should not throw an error if plugin version matches kibana version', function () {
const errorStub = sinon.stub();
settings.plugins[0].version = '1.0.0';
try {
assertVersion(settings);
}
catch (err) {
errorStub(err);
}
expect(errorStub.called).to.be(false);
});
it('should ignore version info after the dash in checks on valid version', function () {
const errorStub = sinon.stub();
settings.plugins[0].version = '1.0.0-foo-bar-version-1.2.3';
try {
assertVersion(settings);
}
catch (err) {
errorStub(err);
}
expect(errorStub.called).to.be(false);
});
it('should ignore version info after the dash in checks on invalid version', function () {
const errorStub = sinon.stub();
settings.plugins[0].version = '2.0.0-foo-bar-version-1.2.3';
try {
assertVersion(settings);
}
catch (err) {
errorStub(err);
}
expect(errorStub.firstCall.args[0]).to.match(/incorrect version/i);
});
});
describe('existingInstall', function () {
let testWorkingPath;
let processExitStub;
beforeEach(function () {
processExitStub = sinon.stub(process, 'exit');
testWorkingPath = join(__dirname, '.test.data');
rimraf.sync(testWorkingPath);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
});
afterEach(function () {
processExitStub.restore();
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
it('should throw an error if the workingPath already exists.', function () {
mkdirp.sync(settings.plugins[0].path);
existingInstall(settings, logger);
expect(logger.error.firstCall.args[0]).to.match(/already exists/);
expect(process.exit.called).to.be(true);
});
it('should not throw an error if the workingPath does not exist.', function () {
existingInstall(settings, logger);
expect(logger.error.called).to.be(false);
});
});
});
});
});

View file

@ -0,0 +1,174 @@
import expect from 'expect.js';
import sinon from 'sinon';
import glob from 'glob-all';
import rimraf from 'rimraf';
import mkdirp from 'mkdirp';
import Logger from '../../lib/logger';
import { extract, getPackData } from '../pack';
import { _downloadSingle } from '../download';
import { join } from 'path';
describe('kibana cli', function () {
describe('pack', function () {
const testWorkingPath = join(__dirname, '.test.data');
const tempArchiveFilePath = join(testWorkingPath, 'archive.part');
const testPluginPath = join(testWorkingPath, '.installedPlugins');
let logger;
const settings = {
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
pluginDir: testPluginPath,
plugin: 'test-plugin'
};
beforeEach(function () {
logger = new Logger(settings);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(testWorkingPath);
mkdirp.sync(testWorkingPath);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
function copyReplyFile(filename) {
const filePath = join(__dirname, 'replies', filename);
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return _downloadSingle(settings, logger, sourceUrl);
}
function shouldReject() {
throw new Error('expected the promise to reject');
}
describe('extract', function () {
//Also only extracts the content from the kibana folder.
//Ignores the others.
it('successfully extract a valid zip', function () {
return copyReplyFile('test_plugin.zip')
.then(() => {
return getPackData(settings, logger);
})
.then(() => {
return extract(settings, logger);
})
.then(() => {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'archive.part',
'README.md',
'index.js',
'package.json',
'public',
'public/app.js',
'extra file only in zip.txt'
];
expect(files.sort()).to.eql(expected.sort());
});
});
});
describe('getPackData', function () {
it('populate settings.plugins', function () {
return copyReplyFile('test_plugin.zip')
.then(() => {
return getPackData(settings, logger);
})
.then(() => {
expect(settings.plugins[0].name).to.be('test-plugin');
expect(settings.plugins[0].folder).to.be('test-plugin');
expect(settings.plugins[0].version).to.be('1.0.0');
expect(settings.plugins[0].platform).to.be(undefined);
});
});
it('populate settings.plugins with multiple plugins', function () {
return copyReplyFile('test_plugin_many.zip')
.then(() => {
return getPackData(settings, logger);
})
.then(() => {
expect(settings.plugins[0].name).to.be('funger-plugin');
expect(settings.plugins[0].file).to.be('kibana/funger-plugin/package.json');
expect(settings.plugins[0].folder).to.be('funger-plugin');
expect(settings.plugins[0].version).to.be('1.0.0');
expect(settings.plugins[0].platform).to.be(undefined);
expect(settings.plugins[1].name).to.be('pdf');
expect(settings.plugins[1].file).to.be('kibana/pdf-linux/package.json');
expect(settings.plugins[1].folder).to.be('pdf-linux');
expect(settings.plugins[1].version).to.be('1.0.0');
expect(settings.plugins[1].platform).to.be('linux');
expect(settings.plugins[2].name).to.be('pdf');
expect(settings.plugins[2].file).to.be('kibana/pdf-win32/package.json');
expect(settings.plugins[2].folder).to.be('pdf-win32');
expect(settings.plugins[2].version).to.be('1.0.0');
expect(settings.plugins[2].platform).to.be('win32');
expect(settings.plugins[3].name).to.be('pdf');
expect(settings.plugins[3].file).to.be('kibana/pdf-win64/package.json');
expect(settings.plugins[3].folder).to.be('pdf-win64');
expect(settings.plugins[3].version).to.be('1.0.0');
expect(settings.plugins[3].platform).to.be('win64');
expect(settings.plugins[4].name).to.be('pdf');
expect(settings.plugins[4].file).to.be('kibana/pdf/package.json');
expect(settings.plugins[4].folder).to.be('pdf');
expect(settings.plugins[4].version).to.be('1.0.0');
expect(settings.plugins[4].platform).to.be(undefined);
expect(settings.plugins[5].name).to.be('test-plugin');
expect(settings.plugins[5].file).to.be('kibana/test-plugin/package.json');
expect(settings.plugins[5].folder).to.be('test-plugin');
expect(settings.plugins[5].version).to.be('1.0.0');
expect(settings.plugins[5].platform).to.be(undefined);
});
});
it('throw an error if there is no kibana plugin', function () {
return copyReplyFile('test_plugin_no_kibana.zip')
.then((data) => {
return getPackData(settings, logger);
})
.then(shouldReject, (err) => {
expect(err.message).to.match(/No kibana plugins found in archive/i);
});
});
it('throw an error with a corrupt zip', function () {
return copyReplyFile('corrupt.zip')
.then((data) => {
return getPackData(settings, logger);
})
.then(shouldReject, (err) => {
expect(err.message).to.match(/error retrieving/i);
});
});
it('throw an error if there an invalid plugin name', function () {
return copyReplyFile('invalid_name.zip')
.then((data) => {
return getPackData(settings, logger);
})
.then(shouldReject, (err) => {
expect(err.message).to.match(/invalid plugin name/i);
});
});
});
});
});

View file

@ -1,23 +1,22 @@
import expect from 'expect.js';
import sinon from 'sinon';
import progressReporter from '../progress_reporter';
import pluginLogger from '../plugin_logger';
import Progress from '../progress';
import Logger from '../../lib/logger';
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('progressReporter', function () {
let logger;
let progress;
let request;
beforeEach(function () {
logger = pluginLogger({ silent: false, quiet: false });
logger = new Logger({ silent: false, quiet: false });
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
progress = progressReporter(logger);
progress = new Progress(logger);
});
afterEach(function () {

View file

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 204 KiB

View file

@ -0,0 +1,3 @@
{
"name": "test-plugin",
}

View file

@ -0,0 +1,228 @@
import path from 'path';
import expect from 'expect.js';
import fromRoot from '../../../utils/from_root';
import { resolve } from 'path';
import { parseMilliseconds, parse, getPlatform } from '../settings';
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('command line option parsing', function () {
describe('parseMilliseconds function', function () {
it('should return 0 for an empty string', function () {
const value = '';
const result = parseMilliseconds(value);
expect(result).to.be(0);
});
it('should return 0 for a number with an invalid unit of measure', function () {
const result = parseMilliseconds('1gigablasts');
expect(result).to.be(0);
});
it('should assume a number with no unit of measure is specified as milliseconds', function () {
const result = parseMilliseconds(1);
expect(result).to.be(1);
const result2 = parseMilliseconds('1');
expect(result2).to.be(1);
});
it('should interpret a number with "s" as the unit of measure as seconds', function () {
const result = parseMilliseconds('5s');
expect(result).to.be(5 * 1000);
});
it('should interpret a number with "second" as the unit of measure as seconds', function () {
const result = parseMilliseconds('5second');
expect(result).to.be(5 * 1000);
});
it('should interpret a number with "seconds" as the unit of measure as seconds', function () {
const result = parseMilliseconds('5seconds');
expect(result).to.be(5 * 1000);
});
it('should interpret a number with "m" as the unit of measure as minutes', function () {
const result = parseMilliseconds('9m');
expect(result).to.be(9 * 1000 * 60);
});
it('should interpret a number with "minute" as the unit of measure as minutes', function () {
const result = parseMilliseconds('9minute');
expect(result).to.be(9 * 1000 * 60);
});
it('should interpret a number with "minutes" as the unit of measure as minutes', function () {
const result = parseMilliseconds('9minutes');
expect(result).to.be(9 * 1000 * 60);
});
});
describe('parse function', function () {
const command = 'plugin name';
let options = {};
const kbnPackage = { version: 1234 };
beforeEach(function () {
options = { pluginDir: fromRoot('installedPlugins') };
});
describe('timeout option', function () {
it('should default to 0 (milliseconds)', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.timeout).to.be(0);
});
it('should set settings.timeout property', function () {
options.timeout = 1234;
const settings = parse(command, options, kbnPackage);
expect(settings.timeout).to.be(1234);
});
});
describe('quiet option', function () {
it('should default to false', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.quiet).to.be(false);
});
it('should set settings.quiet property to true', function () {
options.quiet = true;
const settings = parse(command, options, kbnPackage);
expect(settings.quiet).to.be(true);
});
});
describe('silent option', function () {
it('should default to false', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.silent).to.be(false);
});
it('should set settings.silent property to true', function () {
options.silent = true;
const settings = parse(command, options, kbnPackage);
expect(settings.silent).to.be(true);
});
});
describe('config option', function () {
it('should default to ZLS', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.config).to.be('');
});
it('should set settings.config property', function () {
options.config = 'foo bar baz';
const settings = parse(command, options, kbnPackage);
expect(settings.config).to.be('foo bar baz');
});
});
describe('pluginDir option', function () {
it('should default to installedPlugins', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.pluginDir).to.be(fromRoot('installedPlugins'));
});
it('should set settings.config property', function () {
options.pluginDir = 'foo bar baz';
const settings = parse(command, options, kbnPackage);
expect(settings.pluginDir).to.be('foo bar baz');
});
});
describe('command value', function () {
it('should set settings.plugin property', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.plugin).to.be(command);
});
});
describe('urls collection', function () {
it('should populate the settings.urls property', function () {
const settings = parse(command, options, kbnPackage);
const expected = [
command,
`https://download.elastic.co/packs/${command}/${command}-1234.zip`
];
expect(settings.urls).to.eql(expected);
});
});
describe('workingPath value', function () {
it('should set settings.workingPath property', function () {
options.pluginDir = 'foo/bar/baz';
const settings = parse(command, options, kbnPackage);
const expected = resolve('foo/bar/baz', '.plugin.installing');
expect(settings.workingPath).to.be(expected);
});
});
describe('tempArchiveFile value', function () {
it('should set settings.tempArchiveFile property', function () {
options.pluginDir = 'foo/bar/baz';
const settings = parse(command, options, kbnPackage);
const expected = resolve('foo/bar/baz', '.plugin.installing', 'archive.part');
expect(settings.tempArchiveFile).to.be(expected);
});
});
describe('tempPackageFile value', function () {
it('should set settings.tempPackageFile property', function () {
options.pluginDir = 'foo/bar/baz';
const settings = parse(command, options, kbnPackage);
const expected = resolve('foo/bar/baz', '.plugin.installing', 'package.json');
expect(settings.tempPackageFile).to.be(expected);
});
});
});
});
});
});

View file

@ -0,0 +1,145 @@
import expect from 'expect.js';
import sinon from 'sinon';
import glob from 'glob-all';
import rimraf from 'rimraf';
import mkdirp from 'mkdirp';
import Logger from '../../lib/logger';
import { _downloadSingle } from '../download';
import { join } from 'path';
import { listFiles, extractFiles } from '../zip';
describe('kibana cli', function () {
describe('zip', function () {
const testWorkingPath = join(__dirname, '.test.data');
const tempArchiveFilePath = join(testWorkingPath, 'archive.part');
let logger;
const settings = {
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
plugin: 'test-plugin',
setPlugin: function (plugin) {}
};
function shouldReject() {
throw new Error('expected the promise to reject');
}
beforeEach(function () {
logger = new Logger(settings);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
sinon.stub(settings, 'setPlugin');
rimraf.sync(testWorkingPath);
mkdirp.sync(testWorkingPath);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
settings.setPlugin.restore();
rimraf.sync(testWorkingPath);
});
function copyReplyFile(filename) {
const filePath = join(__dirname, 'replies', filename);
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return _downloadSingle(settings, logger, sourceUrl);
}
describe('listFiles', function () {
it('lists the files in the zip', function () {
return copyReplyFile('test_plugin.zip')
.then(() => {
return listFiles(settings.tempArchiveFile);
})
.then((actual) => {
const expected = [
'elasticsearch/',
'kibana/',
'kibana/test-plugin/',
'kibana/test-plugin/.gitignore',
'kibana/test-plugin/extra file only in zip.txt',
'kibana/test-plugin/index.js',
'kibana/test-plugin/package.json',
'kibana/test-plugin/public/',
'kibana/test-plugin/public/app.js',
'kibana/test-plugin/README.md',
'logstash/'
];
expect(actual).to.eql(expected);
});
});
});
describe('extractFiles', function () {
it('extracts files using the files filter', function () {
return copyReplyFile('test_plugin_many.zip')
.then(() => {
const filter = {
files: [
'kibana/funger-plugin/extra file only in zip.txt',
'kibana/funger-plugin/index.js',
'kibana\\funger-plugin\\package.json'
]
};
return extractFiles(settings.tempArchiveFile, settings.workingPath, 0, filter);
})
.then(() => {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'kibana',
'kibana/funger-plugin',
'kibana/funger-plugin/extra file only in zip.txt',
'kibana/funger-plugin/index.js',
'kibana/funger-plugin/package.json',
'archive.part'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it('extracts files using the paths filter', function () {
return copyReplyFile('test_plugin_many.zip')
.then(() => {
const filter = {
paths: [
'kibana/funger-plugin',
'kibana/test-plugin/public'
]
};
return extractFiles(settings.tempArchiveFile, settings.workingPath, 0, filter);
})
.then(() => {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'archive.part',
'kibana',
'kibana/funger-plugin',
'kibana/funger-plugin/README.md',
'kibana/funger-plugin/extra file only in zip.txt',
'kibana/funger-plugin/index.js',
'kibana/funger-plugin/package.json',
'kibana/funger-plugin/public',
'kibana/funger-plugin/public/app.js',
'kibana/test-plugin',
'kibana/test-plugin/public',
'kibana/test-plugin/public/app.js'
];
expect(files.sort()).to.eql(expected.sort());
});
});
});
});
});

View file

@ -0,0 +1,32 @@
import rimraf from 'rimraf';
import fs from 'fs';
export function cleanPrevious(settings, logger) {
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) {
reject(e);
}
resolve();
} catch (e) {
if (e.code !== 'ENOENT') reject(e);
resolve();
}
});
};
export function cleanArtifacts(settings) {
// delete the working directory.
// At this point we're bailing, so swallow any errors on delete.
try {
rimraf.sync(settings.workingPath);
rimraf.sync(settings.plugins[0].path);
}
catch (e) {} // eslint-disable-line no-empty
};

View file

@ -0,0 +1,40 @@
import downloadHttpFile from './downloaders/http';
import downloadLocalFile from './downloaders/file';
import { parse } from 'url';
export function _downloadSingle(settings, logger, sourceUrl) {
const urlInfo = parse(sourceUrl);
let downloadPromise;
if (/^file/.test(urlInfo.protocol)) {
downloadPromise = downloadLocalFile(logger, decodeURI(urlInfo.path), settings.tempArchiveFile);
} else {
downloadPromise = downloadHttpFile(logger, sourceUrl, settings.tempArchiveFile, settings.timeout);
}
return downloadPromise;
}
//Attempts to download each url in turn until one is successful
export function download(settings, logger) {
const urls = settings.urls.slice(0);
function tryNext() {
const sourceUrl = urls.shift();
if (!sourceUrl) {
throw new Error('No valid url specified.');
}
logger.log(`Attempting to transfer from ${sourceUrl}`);
return _downloadSingle(settings, logger, sourceUrl)
.catch((err) => {
if (err.message === 'ENOTFOUND') {
return tryNext();
}
throw (err);
});
}
return tryNext();
};

View file

@ -1,6 +1,5 @@
import getProgressReporter from '../progress_reporter';
import Progress from '../progress';
import { createWriteStream, createReadStream, unlinkSync, statSync } from 'fs';
import fileType from '../file_type';
function openSourceFile({ sourcePath }) {
try {
@ -18,7 +17,7 @@ function openSourceFile({ sourcePath }) {
}
}
async function copyFile({ readStream, writeStream, progressReporter }) {
async function copyFile({ readStream, writeStream, progress }) {
await new Promise((resolve, reject) => {
// if either stream errors, fail quickly
readStream.on('error', reject);
@ -26,7 +25,7 @@ async function copyFile({ readStream, writeStream, progressReporter }) {
// report progress as we transfer
readStream.on('data', (chunk) => {
progressReporter.progress(chunk.length);
progress.progress(chunk.length);
});
// write the download to the file system
@ -46,21 +45,17 @@ export default async function copyLocalFile(logger, sourcePath, targetPath) {
const writeStream = createWriteStream(targetPath);
try {
const progressReporter = getProgressReporter(logger);
progressReporter.init(fileInfo.size);
const progress = new Progress(logger);
progress.init(fileInfo.size);
await copyFile({ readStream, writeStream, progressReporter });
await copyFile({ readStream, writeStream, progress });
progressReporter.complete();
progress.complete();
} catch (err) {
readStream.close();
writeStream.close();
throw err;
}
// all is well, return our archive type
const archiveType = fileType(sourcePath);
return { archiveType };
} catch (err) {
logger.error(err);
throw err;

View file

@ -1,8 +1,7 @@
import Wreck from 'wreck';
import getProgressReporter from '../progress_reporter';
import Progress from '../progress';
import { fromNode as fn } from 'bluebird';
import { createWriteStream, unlinkSync } from 'fs';
import fileType, { ZIP, TAR } from '../file_type';
function sendRequest({ sourceUrl, timeout }) {
const maxRedirects = 11; //Because this one goes to 11.
@ -25,7 +24,7 @@ function sendRequest({ sourceUrl, timeout }) {
});
}
function downloadResponse({ resp, targetPath, progressReporter }) {
function downloadResponse({ resp, targetPath, progress }) {
return new Promise((resolve, reject) => {
const writeStream = createWriteStream(targetPath);
@ -35,7 +34,7 @@ function downloadResponse({ resp, targetPath, progressReporter }) {
// report progress as we download
resp.on('data', (chunk) => {
progressReporter.progress(chunk.length);
progress.progress(chunk.length);
});
// write the download to the file system
@ -46,19 +45,6 @@ function downloadResponse({ resp, targetPath, progressReporter }) {
});
}
function getArchiveTypeFromResponse(resp, sourceUrl) {
const contentType = (resp.headers['content-type'] || '');
switch (contentType.toLowerCase()) {
case 'application/zip': return ZIP;
case 'application/x-gzip': return TAR;
default:
//If we can't infer the archive type from the content-type header,
//fall back to checking the extension in the url
return fileType(sourceUrl);
}
}
/*
Responsible for managing http transfers
*/
@ -68,20 +54,16 @@ export default async function downloadUrl(logger, sourceUrl, targetPath, timeout
try {
let totalSize = parseFloat(resp.headers['content-length']) || 0;
const progressReporter = getProgressReporter(logger);
progressReporter.init(totalSize);
const progress = new Progress(logger);
progress.init(totalSize);
await downloadResponse({ resp, targetPath, progressReporter });
await downloadResponse({ resp, targetPath, progress });
progressReporter.complete();
progress.complete();
} catch (err) {
req.abort();
throw err;
}
// all is well, return our archive type
const archiveType = getArchiveTypeFromResponse(resp, sourceUrl);
return { archiveType };
} catch (err) {
if (err.message !== 'ENOTFOUND') {
logger.error(err);

View file

@ -0,0 +1,47 @@
import fromRoot from '../../utils/from_root';
import install from './install';
import Logger from '../lib/logger';
import pkg from '../../utils/package_json';
import { parse, parseMilliseconds } from './settings';
function processCommand(command, options) {
let settings;
try {
settings = parse(command, options, pkg);
} catch (ex) {
//The logger has not yet been initialized.
console.error(ex.message);
process.exit(64); // eslint-disable-line no-process-exit
}
const logger = new Logger(settings);
install(settings, logger);
}
export default function pluginInstall(program) {
program
.command('install <plugin/url>')
.option('-q, --quiet', 'disable all process messaging except errors')
.option('-s, --silent', 'disable all process messaging')
.option(
'-c, --config <path>',
'path to the config file',
fromRoot('config/kibana.yml')
)
.option(
'-t, --timeout <duration>',
'length of time before failing; 0 for never fail',
parseMilliseconds
)
.option(
'-d, --plugin-dir <path>',
'path to the directory where plugins are stored',
fromRoot('installedPlugins')
)
.description('install a plugin',
`Common examples:
install xpack
install file:///Path/to/my/xpack.zip
install https://path.to/my/xpack.zip`)
.action(processCommand);
};

View file

@ -0,0 +1,40 @@
import { download } from './download';
import Promise from 'bluebird';
import { cleanPrevious, cleanArtifacts } from './cleanup';
import { extract, getPackData } from './pack';
import { sync as rimrafSync } from 'rimraf';
import { renameSync } from 'fs';
import { existingInstall, rebuildCache, assertVersion } from './kibana';
import mkdirp from 'mkdirp';
const mkdir = Promise.promisify(mkdirp);
export default async function install(settings, logger) {
try {
await cleanPrevious(settings, logger);
await mkdir(settings.workingPath);
await download(settings, logger);
await getPackData(settings, logger);
await extract(settings, logger);
rimrafSync(settings.tempArchiveFile);
existingInstall(settings, logger);
assertVersion(settings);
renameSync(settings.workingPath, settings.plugins[0].path);
await rebuildCache(settings, logger);
logger.log('Plugin installation complete');
} catch (err) {
logger.error(`Plugin installation was unsuccessful due to error "${err.message}"`);
cleanArtifacts(settings);
process.exit(70); // eslint-disable-line no-process-exit
}
}

View file

@ -0,0 +1,59 @@
import _ from 'lodash';
import fromRoot from '../../utils/from_root';
import KbnServer from '../../server/kbn_server';
import readYamlConfig from '../../cli/serve/read_yaml_config';
import { versionSatisfies, cleanVersion } from './version';
import { statSync } from 'fs';
export function existingInstall(settings, logger) {
try {
statSync(settings.plugins[0].path);
logger.error(`Plugin ${settings.plugins[0].name} already exists, please remove before installing a new version`);
process.exit(70); // eslint-disable-line no-process-exit
} catch (e) {
if (e.code !== 'ENOENT') throw e;
}
}
export async function rebuildCache(settings, logger) {
logger.log('Optimizing and caching browser bundles...');
const serverConfig = _.merge(
readYamlConfig(settings.config),
{
env: 'production',
logging: {
silent: settings.silent,
quiet: !settings.silent,
verbose: false
},
optimize: {
useBundleCache: false
},
server: {
autoListen: false
},
plugins: {
initialize: false,
scanDirs: [settings.pluginDir, fromRoot('src/plugins')]
}
}
);
const kbnServer = new KbnServer(serverConfig);
await kbnServer.ready();
await kbnServer.close();
}
export function assertVersion(settings) {
if (!settings.plugins[0].version) {
throw new Error (`Plugin version not found. Check package.json in archive`);
}
const actual = cleanVersion(settings.plugins[0].version);
const expected = cleanVersion(settings.version);
if (!versionSatisfies(actual, expected)) {
throw new Error (`Incorrect version in plugin [${settings.plugins[0].name}]. ` +
`Expected [${expected}]; found [${actual}]`);
}
}

View file

@ -0,0 +1,136 @@
import _ from 'lodash';
import { listFiles, extractFiles } from './zip';
import { resolve } from 'path';
import { sync as rimrafSync } from 'rimraf';
import validate from 'validate-npm-package-name';
/**
* Returns an array of package objects. There will be one for each of
* package.json files in the archive
* @param {object} settings - a plugin installer settings object
*/
async function listPackages(settings) {
const regExp = new RegExp('(kibana/([^/]+))/package.json', 'i');
const archiveFiles = await listFiles(settings.tempArchiveFile);
return _(archiveFiles)
.map(file => file.replace(/\\/g, '/'))
.map(file => file.match(regExp))
.compact()
.map(([ file, _, folder ]) => ({ file, folder }))
.uniq()
.value();
}
/**
* Extracts the package.json files into the workingPath
* @param {object} settings - a plugin installer settings object
* @param {array} packages - array of package objects from listPackages()
*/
async function extractPackageFiles(settings, packages) {
const filter = {
files: packages.map((pkg) => pkg.file)
};
await extractFiles(settings.tempArchiveFile, settings.workingPath, 0, filter);
}
/**
* Deletes the package.json files created by extractPackageFiles()
* @param {object} settings - a plugin installer settings object
*/
function deletePackageFiles(settings) {
const fullPath = resolve(settings.workingPath, 'kibana');
rimrafSync(fullPath);
}
/**
* Checks the plugin name. Will throw an exception if it does not meet
* npm package naming conventions
* @param {object} plugin - a package object from listPackages()
*/
function assertValidPackageName(plugin) {
const validation = validate(plugin.name);
if (!validation.validForNewPackages) {
throw new Error(`Invalid plugin name [${plugin.name}] in package.json`);
}
}
/**
* Examine each package.json file to determine the plugin name,
* version, and platform. Mutates the package objects in the packages array
* @param {object} settings - a plugin installer settings object
* @param {array} packages - array of package objects from listPackages()
*/
async function mergePackageData(settings, packages) {
return packages.map((pkg) => {
const fullPath = resolve(settings.workingPath, pkg.file);
const packageInfo = require(fullPath);
pkg.version = _.get(packageInfo, 'version');
pkg.name = _.get(packageInfo, 'name');
pkg.path = resolve(settings.pluginDir, pkg.name);
const regExp = new RegExp(`${pkg.name}-(.+)`, 'i');
const matches = pkg.folder.match(regExp);
pkg.platform = (matches) ? matches[1] : undefined;
return pkg;
});
}
/**
* Extracts the first plugin in the archive.
* NOTE: This will need to be changed in later versions of the pack installer
* that allow for the installation of more than one plugin at once.
* @param {object} settings - a plugin installer settings object
*/
async function extractArchive(settings) {
const filter = {
paths: [ settings.plugins[0].folder ]
};
await extractFiles(settings.tempArchiveFile, settings.workingPath, 2, filter);
}
/**
* Returns the detailed information about each kibana plugin in the pack.
* TODO: If there are platform specific folders, determine which one to use.
* @param {object} settings - a plugin installer settings object
* @param {object} logger - a plugin installer logger object
*/
export async function getPackData(settings, logger) {
let packages;
try {
logger.log('Retrieving metadata from plugin archive');
packages = await listPackages(settings);
await extractPackageFiles(settings, packages);
await mergePackageData(settings, packages);
await deletePackageFiles(settings);
} catch (err) {
logger.error(err);
throw new Error('Error retrieving metadata from plugin archive');
}
if (packages.length === 0) {
throw new Error('No kibana plugins found in archive');
}
packages.forEach(assertValidPackageName);
settings.plugins = packages;
}
export async function extract(settings, logger) {
try {
logger.log('Extracting plugin archive');
await extractArchive(settings);
logger.log('Extraction complete');
} catch (err) {
logger.error(err);
throw new Error('Error extracting plugin archive');
}
};

View file

@ -0,0 +1,38 @@
/**
* Generates file transfer progress messages
*/
export default class Progress {
constructor(logger) {
const self = this;
self.dotCount = 0;
self.runningTotal = 0;
self.totalSize = 0;
self.logger = logger;
}
init(size) {
this.totalSize = size;
const totalDesc = this.totalSize || 'unknown number of';
this.logger.log(`Transferring ${totalDesc} bytes`, true);
}
progress(size) {
if (!this.totalSize) return;
this.runningTotal += size;
let newDotCount = Math.round(this.runningTotal / this.totalSize * 100 / 5);
if (newDotCount > 20) newDotCount = 20;
for (let i = 0; i < (newDotCount - this.dotCount); i++) {
this.logger.log('.', true);
}
this.dotCount = newDotCount;
}
complete() {
this.logger.log(`Transfer complete`, false);
}
}

View file

@ -0,0 +1,47 @@
import expiry from 'expiry-js';
import { intersection } from 'lodash';
import { resolve } from 'path';
import { arch, platform } from 'os';
function generateUrls({ version, plugin }) {
return [
plugin,
`https://download.elastic.co/packs/${plugin}/${plugin}-${version}.zip`
];
}
export function parseMilliseconds(val) {
let result;
try {
const timeVal = expiry(val);
result = timeVal.asMilliseconds();
} catch (ex) {
result = 0;
}
return result;
};
export function parse(command, options, kbnPackage) {
const settings = {
timeout: options.timeout || 0,
quiet: options.quiet || false,
silent: options.silent || false,
config: options.config || '',
plugin: command,
version: kbnPackage.version,
pluginDir: options.pluginDir || ''
};
settings.urls = generateUrls(settings);
settings.workingPath = resolve(settings.pluginDir, '.plugin.installing');
settings.tempArchiveFile = resolve(settings.workingPath, 'archive.part');
settings.tempPackageFile = resolve(settings.workingPath, 'package.json');
settings.setPlugin = function (plugin) {
settings.plugin = plugin;
settings.pluginPath = resolve(settings.pluginDir, settings.plugin.name);
};
return settings;
};

View file

@ -0,0 +1,15 @@
import semver from 'semver';
export function versionSatisfies(cleanActual, cleanExpected) {
try {
return (cleanActual === cleanExpected);
} catch (err) {
return false;
}
}
export function cleanVersion(version) {
const match = version.match(/\d+\.\d+\.\d+/);
if (!match) return version;
return match[0];
}

View file

@ -0,0 +1,94 @@
import _ from 'lodash';
import DecompressZip from '@bigfunger/decompress-zip';
const SYMBOLIC_LINK = 'SymbolicLink';
/**
* Creates a filter function to be consumed by extractFiles that filters by
* an array of files
* @param {array} files - an array of full file paths to extract. Should match
* exactly a value from listFiles
*/
function extractFilterFromFiles(files) {
const filterFiles = files.map((file) => file.replace(/\\/g, '/'));
return function filterByFiles(file) {
if (file.type === SYMBOLIC_LINK) return false;
const path = file.path.replace(/\\/g, '/');
return _.includes(filterFiles, path);
};
}
/**
* Creates a filter function to be consumed by extractFiles that filters by
* an array of root paths
* @param {array} paths - an array of root paths from the archive. All files and
* folders will be extracted recursively using these paths as roots.
*/
function extractFilterFromPaths(paths) {
return function filterByRootPath(file) {
if (file.type === SYMBOLIC_LINK) return false;
return paths.some(path => {
const regex = new RegExp(`${path}($|/)`, 'i');
return file.parent.match(regex);
});
};
}
/**
* Creates a filter function to be consumed by extractFiles
* @param {object} filter - an object with either a files or paths property.
*/
function extractFilter(filter) {
if (filter.files) return extractFilterFromFiles(filter.files);
if (filter.paths) return extractFilterFromPaths(filter.paths);
return _.noop;
}
/**
* Extracts files from a zip archive to a file path using a filter function
* @param {string} zipPath - file path to a zip archive
* @param {string} targetPath - directory path to where the files should
* extracted
* @param {integer} strip - Number of nested directories within the archive
* that should be ignored when determining the target path of an archived
* file.
* @param {function} filter - A function that accepts a single parameter 'file'
* and returns true if the file should be extracted from the archive
*/
export async function extractFiles(zipPath, targetPath, strip, filter) {
await new Promise((resolve, reject) => {
const unzipper = new DecompressZip(zipPath);
unzipper.on('error', reject);
unzipper.extract({
path: targetPath,
strip: strip,
filter: extractFilter(filter)
});
unzipper.on('extract', resolve);
});
}
/**
* Returns all files within an archive
* @param {string} zipPath - file path to a zip archive
* @returns {array} all files within an archive with their relative paths
*/
export async function listFiles(zipPath) {
return await new Promise((resolve, reject) => {
const unzipper = new DecompressZip(zipPath);
unzipper.on('error', reject);
unzipper.on('list', (files) => {
files = files.map((file) => file.replace(/\\/g, '/'));
resolve(files);
});
unzipper.list();
});
}

View file

@ -1,6 +1,6 @@
import expect from 'expect.js';
import sinon from 'sinon';
import pluginLogger from '../plugin_logger';
import Logger from '../logger';
describe('kibana cli', function () {
@ -20,7 +20,7 @@ describe('kibana cli', function () {
});
it('should log messages to the console and append a new line', function () {
logger = pluginLogger({ silent: false, quiet: false });
logger = new Logger({ silent: false, quiet: false });
const message = 'this is my message';
logger.log(message);
@ -31,7 +31,7 @@ describe('kibana cli', function () {
});
it('should log messages to the console and append not append a new line', function () {
logger = pluginLogger({ silent: false, quiet: false });
logger = new Logger({ silent: false, quiet: false });
for (let i = 0; i < 10; i++) {
logger.log('.', true);
}
@ -54,7 +54,7 @@ describe('kibana cli', function () {
});
it('should not log any messages when quiet is set', function () {
logger = pluginLogger({ silent: false, quiet: true });
logger = new Logger({ silent: false, quiet: true });
const message = 'this is my message';
logger.log(message);
@ -68,7 +68,7 @@ describe('kibana cli', function () {
});
it('should not log any messages when silent is set', function () {
logger = pluginLogger({ silent: true, quiet: false });
logger = new Logger({ silent: true, quiet: false });
const message = 'this is my message';
logger.log(message);
@ -94,7 +94,7 @@ describe('kibana cli', function () {
});
it('should log error messages to the console and append a new line', function () {
logger = pluginLogger({ silent: false, quiet: false });
logger = new Logger({ silent: false, quiet: false });
const message = 'this is my error';
logger.error(message);
@ -102,7 +102,7 @@ describe('kibana cli', function () {
});
it('should log error messages to the console when quiet is set', function () {
logger = pluginLogger({ silent: false, quiet: true });
logger = new Logger({ silent: false, quiet: true });
const message = 'this is my error';
logger.error(message);
@ -110,7 +110,7 @@ describe('kibana cli', function () {
});
it('should not log any error messages when silent is set', function () {
logger = pluginLogger({ silent: true, quiet: false });
logger = new Logger({ silent: true, quiet: false });
const message = 'this is my error';
logger.error(message);

View file

@ -0,0 +1,46 @@
/**
* Logs messages and errors
*/
export default class Logger {
constructor(settings) {
this.previousLineEnded = true;
this.silent = !!settings.silent;
this.quiet = !!settings.quiet;
}
log(data, sameLine) {
if (this.silent || this.quiet) return;
if (!sameLine && !this.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');
this.previousLineEnded = !sameLine;
}
error(data) {
if (this.silent) return;
if (!this.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`);
this.previousLineEnded = true;
};
}

View file

@ -0,0 +1,75 @@
import expect from 'expect.js';
import sinon from 'sinon';
import rimraf from 'rimraf';
import mkdirp from 'mkdirp';
import Logger from '../../lib/logger';
import list from '../list';
import { join } from 'path';
import { writeFileSync } from 'fs';
describe('kibana cli', function () {
describe('plugin lister', function () {
const pluginDir = join(__dirname, '.test.data');
let logger;
const settings = {
pluginDir: pluginDir
};
beforeEach(function () {
logger = new Logger(settings);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(pluginDir);
mkdirp.sync(pluginDir);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(pluginDir);
});
it('list all of the folders in the plugin folder', function () {
mkdirp.sync(join(pluginDir, 'plugin1'));
mkdirp.sync(join(pluginDir, 'plugin2'));
mkdirp.sync(join(pluginDir, 'plugin3'));
list(settings, logger);
expect(logger.log.calledWith('plugin1')).to.be(true);
expect(logger.log.calledWith('plugin2')).to.be(true);
expect(logger.log.calledWith('plugin3')).to.be(true);
});
it('ignore folders that start with a period', function () {
mkdirp.sync(join(pluginDir, '.foo'));
mkdirp.sync(join(pluginDir, 'plugin1'));
mkdirp.sync(join(pluginDir, 'plugin2'));
mkdirp.sync(join(pluginDir, 'plugin3'));
mkdirp.sync(join(pluginDir, '.bar'));
list(settings, logger);
expect(logger.log.calledWith('.foo')).to.be(false);
expect(logger.log.calledWith('.bar')).to.be(false);
});
it('list should only list folders', function () {
mkdirp.sync(join(pluginDir, 'plugin1'));
mkdirp.sync(join(pluginDir, 'plugin2'));
mkdirp.sync(join(pluginDir, 'plugin3'));
writeFileSync(join(pluginDir, 'plugin4'), 'This is a file, and not a folder.');
list(settings, logger);
expect(logger.log.calledWith('plugin1')).to.be(true);
expect(logger.log.calledWith('plugin2')).to.be(true);
expect(logger.log.calledWith('plugin3')).to.be(true);
});
});
});

View file

@ -0,0 +1,44 @@
import path from 'path';
import expect from 'expect.js';
import fromRoot from '../../../utils/from_root';
import { resolve } from 'path';
import { parseMilliseconds, parse } from '../settings';
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('command line option parsing', function () {
describe('parse function', function () {
let command;
const options = {};
beforeEach(function () {
command = { pluginDir: fromRoot('installedPlugins') };
});
describe('pluginDir option', function () {
it('should default to installedPlugins', function () {
const settings = parse(command, options);
expect(settings.pluginDir).to.be(fromRoot('installedPlugins'));
});
it('should set settings.config property', function () {
command.pluginDir = 'foo bar baz';
const settings = parse(command, options);
expect(settings.pluginDir).to.be('foo bar baz');
});
});
});
});
});
});

View file

@ -0,0 +1,30 @@
import fromRoot from '../../utils/from_root';
import list from './list';
import Logger from '../lib/logger';
import { parse } from './settings';
function processCommand(command, options) {
let settings;
try {
settings = parse(command, options);
} catch (ex) {
//The logger has not yet been initialized.
console.error(ex.message);
process.exit(64); // eslint-disable-line no-process-exit
}
const logger = new Logger(settings);
list(settings, logger);
}
export default function pluginList(program) {
program
.command('list')
.option(
'-d, --plugin-dir <path>',
'path to the directory where plugins are stored',
fromRoot('installedPlugins')
)
.description('list installed plugins')
.action(processCommand);
};

View file

@ -0,0 +1,14 @@
import { statSync, readdirSync } from 'fs';
import { join } from 'path';
export default function list(settings, logger) {
readdirSync(settings.pluginDir)
.forEach((filename) => {
const stat = statSync(join(settings.pluginDir, filename));
if (stat.isDirectory() && filename[0] !== '.') {
logger.log(filename);
}
});
logger.log(''); //intentional blank line for aesthetics
}

View file

@ -0,0 +1,9 @@
import { resolve } from 'path';
export function parse(command, options) {
const settings = {
pluginDir: command.pluginDir || ''
};
return settings;
};

View file

@ -0,0 +1,68 @@
import expect from 'expect.js';
import sinon from 'sinon';
import glob from 'glob-all';
import rimraf from 'rimraf';
import mkdirp from 'mkdirp';
import Logger from '../../lib/logger';
import remove from '../remove';
import { join } from 'path';
import { writeFileSync } from 'fs';
describe('kibana cli', function () {
describe('plugin remover', function () {
const pluginDir = join(__dirname, '.test.data');
let processExitStub;
let logger;
const settings = { pluginDir };
beforeEach(function () {
processExitStub = sinon.stub(process, 'exit');
logger = new Logger(settings);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(pluginDir);
mkdirp.sync(pluginDir);
});
afterEach(function () {
processExitStub.restore();
logger.log.restore();
logger.error.restore();
rimraf.sync(pluginDir);
});
it('throw an error if the plugin is not installed.', function () {
settings.pluginPath = join(pluginDir, 'foo');
settings.plugin = 'foo';
remove(settings, logger);
expect(logger.error.firstCall.args[0]).to.match(/not installed/);
expect(process.exit.called).to.be(true);
});
it('throw an error if the specified plugin is not a folder.', function () {
writeFileSync(join(pluginDir, 'foo'), 'This is a file, and not a folder.');
remove(settings, logger);
expect(logger.error.firstCall.args[0]).to.match(/not a plugin/);
expect(process.exit.called).to.be(true);
});
it('delete the specified folder.', function () {
settings.pluginPath = join(pluginDir, 'foo');
mkdirp.sync(join(pluginDir, 'foo'));
mkdirp.sync(join(pluginDir, 'bar'));
remove(settings, logger);
const files = glob.sync('**/*', { cwd: pluginDir });
const expected = ['bar'];
expect(files.sort()).to.eql(expected.sort());
});
});
});

View file

@ -0,0 +1,106 @@
import path from 'path';
import expect from 'expect.js';
import fromRoot from '../../../utils/from_root';
import { resolve } from 'path';
import { parseMilliseconds, parse } from '../settings';
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('command line option parsing', function () {
describe('parse function', function () {
const command = 'plugin name';
let options = {};
const kbnPackage = { version: 1234 };
beforeEach(function () {
options = { pluginDir: fromRoot('installedPlugins') };
});
describe('quiet option', function () {
it('should default to false', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.quiet).to.be(false);
});
it('should set settings.quiet property to true', function () {
options.quiet = true;
const settings = parse(command, options, kbnPackage);
expect(settings.quiet).to.be(true);
});
});
describe('silent option', function () {
it('should default to false', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.silent).to.be(false);
});
it('should set settings.silent property to true', function () {
options.silent = true;
const settings = parse(command, options, kbnPackage);
expect(settings.silent).to.be(true);
});
});
describe('config option', function () {
it('should default to ZLS', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.config).to.be('');
});
it('should set settings.config property', function () {
options.config = 'foo bar baz';
const settings = parse(command, options, kbnPackage);
expect(settings.config).to.be('foo bar baz');
});
});
describe('pluginDir option', function () {
it('should default to installedPlugins', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.pluginDir).to.be(fromRoot('installedPlugins'));
});
it('should set settings.config property', function () {
options.pluginDir = 'foo bar baz';
const settings = parse(command, options, kbnPackage);
expect(settings.pluginDir).to.be('foo bar baz');
});
});
describe('command value', function () {
it('should set settings.plugin property', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.plugin).to.be(command);
});
});
});
});
});
});

View file

@ -0,0 +1,39 @@
import fromRoot from '../../utils/from_root';
import remove from './remove';
import Logger from '../lib/logger';
import { parse } from './settings';
function processCommand(command, options) {
let settings;
try {
settings = parse(command, options);
} catch (ex) {
//The logger has not yet been initialized.
console.error(ex.message);
process.exit(64); // eslint-disable-line no-process-exit
}
const logger = new Logger(settings);
remove(settings, logger);
}
export default function pluginRemove(program) {
program
.command('remove <plugin>')
.option('-q, --quiet', 'disable all process messaging except errors')
.option('-s, --silent', 'disable all process messaging')
.option(
'-c, --config <path>',
'path to the config file',
fromRoot('config/kibana.yml')
)
.option(
'-d, --plugin-dir <path>',
'path to the directory where plugins are stored',
fromRoot('installedPlugins')
)
.description('remove a plugin',
`common examples:
remove xpack`)
.action(processCommand);
};

View file

@ -0,0 +1,23 @@
import { statSync } from 'fs';
import rimraf from 'rimraf';
export default function remove(settings, logger) {
try {
let stat;
try {
stat = statSync(settings.pluginPath);
} catch (e) {
throw new Error(`Plugin [${settings.plugin}] is not installed`);
}
if (!stat.isDirectory()) {
throw new Error(`[${settings.plugin}] is not a plugin`);
}
logger.log(`Removing ${settings.plugin}...`);
rimraf.sync(settings.pluginPath);
} catch (err) {
logger.error(`Unable to remove plugin because of error: "${err.message}"`);
process.exit(74); // eslint-disable-line no-process-exit
}
}

View file

@ -0,0 +1,15 @@
import { resolve } from 'path';
export function parse(command, options) {
const settings = {
quiet: options.quiet || false,
silent: options.silent || false,
config: options.config || '',
pluginDir: options.pluginDir || '',
plugin: command
};
settings.pluginPath = resolve(settings.pluginDir, settings.plugin);
return settings;
};