[bin/kibana-plugin] support KP plugins instead (#74604)

* [cli/kibana-plugin] support KP plugins instead

* fix some Logger imports from cli/kibana_keystore

Co-authored-by: spalger <spalger@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Spencer 2020-08-12 16:38:08 -07:00 committed by GitHub
parent 981fdda966
commit a735a9f825
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 477 additions and 991 deletions

View file

@ -276,7 +276,6 @@
"url-loader": "2.2.0",
"uuid": "3.3.2",
"val-loader": "^1.1.1",
"validate-npm-package-name": "2.2.2",
"vega": "^5.13.0",
"vega-lite": "^4.13.1",
"vega-schema-url-parser": "^1.1.0",

View file

@ -97,8 +97,6 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(511);
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; });
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; });
/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(145);
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjects", function() { return _utils_projects__WEBPACK_IMPORTED_MODULE_2__["getProjects"]; });
@ -59477,9 +59475,6 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(512);
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; });
/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(748);
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; });
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
@ -59500,7 +59495,6 @@ __webpack_require__.r(__webpack_exports__);
*/
/***/ }),
/* 512 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
@ -90331,71 +90325,5 @@ NestedError.prototype.name = 'NestedError';
module.exports = NestedError;
/***/ }),
/* 748 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return prepareExternalProjectDependencies; });
/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(164);
/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(163);
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* All external projects are located within `./plugins/{plugin}` relative
* to the Kibana root directory or `../kibana-extra/{plugin}` relative
* to Kibana itself.
*/
const isKibanaDep = depVersion => // For ../kibana-extra/ directory (legacy only)
depVersion.includes('../../kibana/packages/') || // For plugins/ directory
depVersion.includes('../../packages/');
/**
* This prepares the dependencies for an _external_ project.
*/
async function prepareExternalProjectDependencies(projectPath) {
const project = await _utils_project__WEBPACK_IMPORTED_MODULE_1__["Project"].fromPath(projectPath);
if (!project.hasDependencies()) {
return;
}
const deps = project.allDependencies;
for (const depName of Object.keys(deps)) {
const depVersion = deps[depName]; // Kibana currently only supports `link:` dependencies on Kibana's own
// packages, as these are packaged into the `node_modules` folder when
// Kibana is built, so we don't need to take any action to enable
// `require(...)` to resolve for these packages.
if (Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_0__["isLinkDependency"])(depVersion) && !isKibanaDep(depVersion)) {
// For non-Kibana packages we need to set up symlinks during the
// installation process, but this is not something we support yet.
throw new Error('This plugin is using `link:` dependencies for non-Kibana packages');
}
}
}
/***/ })
/******/ ]);

View file

@ -18,7 +18,7 @@
*/
export { run } from './cli';
export { buildProductionProjects, prepareExternalProjectDependencies } from './production';
export { buildProductionProjects } from './production';
export { getProjects } from './utils/projects';
export { Project } from './utils/project';
export { copyWorkspacePackages } from './utils/workspaces';

View file

@ -18,4 +18,3 @@
*/
export { buildProductionProjects } from './build_production_projects';
export { prepareExternalProjectDependencies } from './prepare_project_dependencies';

View file

@ -1,40 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { join, resolve } from 'path';
import { prepareExternalProjectDependencies } from './prepare_project_dependencies';
const packagesFixtures = resolve(__dirname, '__fixtures__/external_packages');
test('does nothing when Kibana `link:` dependencies', async () => {
const projectPath = join(packagesFixtures, 'with_kibana_link_deps');
// We're checking for undefined, but we don't really care about what's
// returned, we only care about it resolving.
await expect(prepareExternalProjectDependencies(projectPath)).resolves.toBeUndefined();
});
test('throws if non-Kibana `link` dependencies', async () => {
const projectPath = join(packagesFixtures, 'with_other_link_deps');
await expect(prepareExternalProjectDependencies(projectPath)).rejects.toThrow(
'This plugin is using `link:` dependencies for non-Kibana packages'
);
});

View file

@ -1,59 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { isLinkDependency } from '../utils/package_json';
import { Project } from '../utils/project';
/**
* All external projects are located within `./plugins/{plugin}` relative
* to the Kibana root directory or `../kibana-extra/{plugin}` relative
* to Kibana itself.
*/
const isKibanaDep = (depVersion: string) =>
// For ../kibana-extra/ directory (legacy only)
depVersion.includes('../../kibana/packages/') ||
// For plugins/ directory
depVersion.includes('../../packages/');
/**
* This prepares the dependencies for an _external_ project.
*/
export async function prepareExternalProjectDependencies(projectPath: string) {
const project = await Project.fromPath(projectPath);
if (!project.hasDependencies()) {
return;
}
const deps = project.allDependencies;
for (const depName of Object.keys(deps)) {
const depVersion = deps[depName];
// Kibana currently only supports `link:` dependencies on Kibana's own
// packages, as these are packaged into the `node_modules` folder when
// Kibana is built, so we don't need to take any action to enable
// `require(...)` to resolve for these packages.
if (isLinkDependency(depVersion) && !isKibanaDep(depVersion)) {
// For non-Kibana packages we need to set up symlinks during the
// installation process, but this is not something we support yet.
throw new Error('This plugin is using `link:` dependencies for non-Kibana packages');
}
}
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import Logger from '../cli_plugin/lib/logger';
import { Logger } from '../cli_plugin/lib/logger';
import { confirm, question } from '../legacy/server/utils';
import { createPromiseFromStreams, createConcatStream } from '../legacy/utils';

View file

@ -41,7 +41,7 @@ import { PassThrough } from 'stream';
import { Keystore } from '../legacy/server/keystore';
import { add } from './add';
import Logger from '../cli_plugin/lib/logger';
import { Logger } from '../cli_plugin/lib/logger';
import * as prompt from '../legacy/server/utils/prompt';
describe('Kibana keystore', () => {

View file

@ -17,7 +17,7 @@
* under the License.
*/
import Logger from '../cli_plugin/lib/logger';
import { Logger } from '../cli_plugin/lib/logger';
import { confirm } from '../legacy/server/utils';
export async function create(keystore, command, options) {

View file

@ -40,7 +40,7 @@ import sinon from 'sinon';
import { Keystore } from '../legacy/server/keystore';
import { create } from './create';
import Logger from '../cli_plugin/lib/logger';
import { Logger } from '../cli_plugin/lib/logger';
import * as prompt from '../legacy/server/utils/prompt';
describe('Kibana keystore', () => {

View file

@ -20,7 +20,7 @@
import { existsSync } from 'fs';
import { join } from 'path';
import Logger from '../cli_plugin/lib/logger';
import { Logger } from '../cli_plugin/lib/logger';
import { getConfigDirectory, getDataPath } from '../core/server/path';
export function getKeystore() {

View file

@ -18,7 +18,7 @@
*/
import { getKeystore } from './get_keystore';
import Logger from '../cli_plugin/lib/logger';
import { Logger } from '../cli_plugin/lib/logger';
import fs from 'fs';
import sinon from 'sinon';

View file

@ -17,7 +17,7 @@
* under the License.
*/
import Logger from '../cli_plugin/lib/logger';
import { Logger } from '../cli_plugin/lib/logger';
export function list(keystore, command, options = {}) {
const logger = new Logger(options);

View file

@ -38,7 +38,7 @@ jest.mock('fs', () => ({
import sinon from 'sinon';
import { Keystore } from '../legacy/server/keystore';
import { list } from './list';
import Logger from '../cli_plugin/lib/logger';
import { Logger } from '../cli_plugin/lib/logger';
describe('Kibana keystore', () => {
describe('list', () => {

View file

@ -17,12 +17,11 @@
* under the License.
*/
import _ from 'lodash';
import { pkg } from '../core/server/utils';
import Command from '../cli/command';
import listCommand from './list';
import installCommand from './install';
import removeCommand from './remove';
import { listCommand } from './list';
import { installCommand } from './install';
import { removeCommand } from './remove';
const argv = process.env.kbnWorkerArgv
? JSON.parse(process.env.kbnWorkerArgv)
@ -44,8 +43,12 @@ program
.command('help <command>')
.description('get the help for a specific command')
.action(function (cmdName) {
const cmd = _.find(program.commands, { _name: cmdName });
if (!cmd) return program.error(`unknown command ${cmdName}`);
const cmd = program.commands.find((c) => c._name === cmdName);
if (!cmd) {
return program.error(`unknown command ${cmdName}`);
}
cmd.help();
});

View file

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

View file

@ -45,6 +45,5 @@ export function cleanArtifacts(settings) {
// At this point we're bailing, so swallow any errors on delete.
try {
del.sync(settings.workingPath);
del.sync(settings.plugins[0].path);
} catch (e) {} // eslint-disable-line no-empty
}

View file

@ -22,7 +22,7 @@ import fs from 'fs';
import del from 'del';
import { cleanPrevious, cleanArtifacts } from './cleanup';
import Logger from '../lib/logger';
import { Logger } from '../lib/logger';
describe('kibana cli', function () {
describe('plugin installer', function () {

View file

@ -17,11 +17,12 @@
* under the License.
*/
import downloadHttpFile from './downloaders/http';
import downloadLocalFile from './downloaders/file';
import { UnsupportedProtocolError } from '../lib/errors';
import { parse } from 'url';
import { UnsupportedProtocolError } from '../lib/errors';
import { downloadHttpFile } from './downloaders/http';
import { downloadLocalFile } from './downloaders/file';
function _isWindows() {
return /^win/.test(process.platform);
}

View file

@ -17,16 +17,18 @@
* under the License.
*/
import Fs from 'fs';
import { join } from 'path';
import http from 'http';
import sinon from 'sinon';
import nock from 'nock';
import glob from 'glob-all';
import del from 'del';
import Fs from 'fs';
import Logger from '../lib/logger';
import { Logger } from '../lib/logger';
import { UnsupportedProtocolError } from '../lib/errors';
import { download, _downloadSingle, _getFilePath, _checkFilePathDeprecation } from './download';
import { join } from 'path';
import http from 'http';
describe('kibana cli', function () {
describe('plugin downloader', function () {

View file

@ -17,9 +17,10 @@
* under the License.
*/
import Progress from '../progress';
import { createWriteStream, createReadStream, statSync } from 'fs';
import { Progress } from '../progress';
function openSourceFile({ sourcePath }) {
try {
const fileInfo = statSync(sourcePath);
@ -58,7 +59,7 @@ async function copyFile({ readStream, writeStream, progress }) {
/*
// Responsible for managing local file transfers
*/
export default async function copyLocalFile(logger, sourcePath, targetPath) {
export async function downloadLocalFile(logger, sourcePath, targetPath) {
try {
const { readStream, fileInfo } = openSourceFile({ sourcePath });
const writeStream = createWriteStream(targetPath);

View file

@ -17,13 +17,15 @@
* under the License.
*/
import Wreck from '@hapi/wreck';
import Progress from '../progress';
import { createWriteStream } from 'fs';
import Wreck from '@hapi/wreck';
import HttpProxyAgent from 'http-proxy-agent';
import HttpsProxyAgent from 'https-proxy-agent';
import { getProxyForUrl } from 'proxy-from-env';
import { Progress } from '../progress';
function getProxyAgent(sourceUrl, logger) {
const proxy = getProxyForUrl(sourceUrl);
@ -91,7 +93,7 @@ function downloadResponse({ resp, targetPath, progress }) {
/*
Responsible for managing http transfers
*/
export default async function downloadUrl(logger, sourceUrl, targetPath, timeout) {
export async function downloadHttpFile(logger, sourceUrl, targetPath, timeout) {
try {
const { req, resp } = await sendRequest({ sourceUrl, timeout }, logger);

View file

@ -17,13 +17,12 @@
* under the License.
*/
import { fromRoot, pkg } from '../../core/server/utils';
import install from './install';
import Logger from '../lib/logger';
import { pkg } from '../../core/server/utils';
import { install } from './install';
import { Logger } from '../lib/logger';
import { getConfigPath } from '../../core/server/path';
import { parse, parseMilliseconds } from './settings';
import logWarnings from '../lib/log_warnings';
import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option';
import { logWarnings } from '../lib/log_warnings';
function processCommand(command, options) {
let settings;
@ -37,12 +36,11 @@ function processCommand(command, options) {
const logger = new Logger(settings);
warnIfUsingPluginDirOption(settings, fromRoot('plugins'), logger);
logWarnings(settings, logger);
install(settings, logger);
}
export default function pluginInstall(program) {
export function installCommand(program) {
program
.command('install <plugin/url>')
.option('-q, --quiet', 'disable all process messaging except errors')
@ -53,15 +51,9 @@ export default function pluginInstall(program) {
'length of time before failing; 0 for never fail',
parseMilliseconds
)
.option(
'-d, --plugin-dir <path>',
'path to the directory where plugins are stored (DEPRECATED, known to not work for all plugins)',
fromRoot('plugins')
)
.description(
'install a plugin',
`Common examples:
install x-pack
install file:///Path/to/my/x-pack.zip
install https://path.to/my/x-pack.zip`
)

View file

@ -18,7 +18,8 @@
*/
import sinon from 'sinon';
import index from './index';
import { installCommand } from './index';
describe('kibana cli', function () {
describe('plugin installer', function () {
@ -41,7 +42,7 @@ describe('kibana cli', function () {
it('should define the command', function () {
sinon.spy(program, 'command');
index(program);
installCommand(program);
expect(program.command.calledWith('install <plugin/url>')).toBe(true);
program.command.restore();
@ -50,7 +51,7 @@ describe('kibana cli', function () {
it('should define the description', function () {
sinon.spy(program, 'description');
index(program);
installCommand(program);
expect(program.description.calledWith('install a plugin')).toBe(true);
program.description.restore();
@ -59,9 +60,9 @@ describe('kibana cli', function () {
it('should define the command line options', function () {
const spy = sinon.spy(program, 'option');
const options = [/-q/, /-s/, /-c/, /-t/, /-d/];
const options = [/-q/, /-s/, /-c/, /-t/];
index(program);
installCommand(program);
for (let i = 0; i < spy.callCount; i++) {
const call = spy.getCall(i);
@ -80,7 +81,7 @@ describe('kibana cli', function () {
it('should call the action function', function () {
sinon.spy(program, 'action');
index(program);
installCommand(program);
expect(program.action.calledOnce).toBe(true);
program.action.restore();

View file

@ -19,20 +19,20 @@
import Fs from 'fs';
import { promisify } from 'util';
import path from 'path';
import del from 'del';
import { download } from './download';
import path from 'path';
import { cleanPrevious, cleanArtifacts } from './cleanup';
import { extract, getPackData } from './pack';
import { renamePlugin } from './rename';
import del from 'del';
import { errorIfXPackInstall } from '../lib/error_if_x_pack';
import { existingInstall, assertVersion } from './kibana';
import { prepareExternalProjectDependencies } from '@kbn/pm';
const mkdir = promisify(Fs.mkdir);
export default async function install(settings, logger) {
export async function install(settings, logger) {
try {
errorIfXPackInstall(settings, logger);
@ -52,12 +52,8 @@ export default async function install(settings, logger) {
assertVersion(settings);
await prepareExternalProjectDependencies(settings.workingPath);
await renamePlugin(
settings.workingPath,
path.join(settings.pluginDir, settings.plugins[0].name)
);
const targetDir = path.join(settings.pluginDir, settings.plugins[0].id);
await renamePlugin(settings.workingPath, targetDir);
logger.log('Plugin installation complete');
} catch (err) {

View file

@ -18,15 +18,16 @@
*/
import path from 'path';
import { versionSatisfies, cleanVersion } from '../../legacy/utils/version';
import { statSync } from 'fs';
import { versionSatisfies, cleanVersion } from '../../legacy/utils/version';
export function existingInstall(settings, logger) {
try {
statSync(path.join(settings.pluginDir, settings.plugins[0].name));
statSync(path.join(settings.pluginDir, settings.plugins[0].id));
logger.error(
`Plugin ${settings.plugins[0].name} already exists, please remove before installing a new version`
`Plugin ${settings.plugins[0].id} already exists, please remove before installing a new version`
);
process.exit(70);
} catch (e) {
@ -37,7 +38,7 @@ export function existingInstall(settings, logger) {
export function assertVersion(settings) {
if (!settings.plugins[0].kibanaVersion) {
throw new Error(
`Plugin package.json is missing both a version property (required) and a kibana.version property (optional).`
`Plugin kibana.json is missing both a version property (required) and a kibanaVersion property (optional).`
);
}
@ -45,7 +46,7 @@ export function assertVersion(settings) {
const expected = cleanVersion(settings.version);
if (!versionSatisfies(actual, expected)) {
throw new Error(
`Plugin ${settings.plugins[0].name} [${actual}] is incompatible with Kibana [${expected}]`
`Plugin ${settings.plugins[0].id} [${actual}] is incompatible with Kibana [${expected}]`
);
}
}

View file

@ -17,12 +17,14 @@
* under the License.
*/
import sinon from 'sinon';
import Logger from '../lib/logger';
import { join } from 'path';
import del from 'del';
import fs from 'fs';
import sinon from 'sinon';
import del from 'del';
import { existingInstall, assertVersion } from './kibana';
import { Logger } from '../lib/logger';
jest.spyOn(fs, 'statSync');
@ -42,7 +44,7 @@ describe('kibana cli', function () {
tempArchiveFile: tempArchiveFilePath,
plugin: 'test-plugin',
version: '1.0.0',
plugins: [{ name: 'foo' }],
plugins: [{ id: 'foo' }],
pluginDir,
};
@ -69,7 +71,10 @@ describe('kibana cli', function () {
plugin: 'test-plugin',
version: '5.0.0-SNAPSHOT',
plugins: [
{ name: 'foo', path: join(testWorkingPath, 'foo'), kibanaVersion: '5.0.0-SNAPSHOT' },
{
id: 'foo',
kibanaVersion: '5.0.0-SNAPSHOT',
},
],
};
@ -77,15 +82,17 @@ describe('kibana cli', function () {
});
it('should throw an error if plugin is missing a kibana version.', function () {
expect(() => assertVersion(settings)).toThrow(
/plugin package\.json is missing both a version property/i
expect(() => assertVersion(settings)).toThrowErrorMatchingInlineSnapshot(
`"Plugin kibana.json is missing both a version property (required) and a kibanaVersion property (optional)."`
);
});
it('should throw an error if plugin kibanaVersion does not match kibana version', function () {
settings.plugins[0].kibanaVersion = '1.2.3.4';
expect(() => assertVersion(settings)).toThrow(/incompatible with Kibana/i);
expect(() => assertVersion(settings)).toThrowErrorMatchingInlineSnapshot(
`"Plugin foo [1.2.3] is incompatible with Kibana [1.0.0]"`
);
});
it('should not throw an error if plugin kibanaVersion matches kibana version', function () {
@ -103,7 +110,9 @@ describe('kibana cli', function () {
it('should ignore version info after the dash in checks on invalid version', function () {
settings.plugins[0].kibanaVersion = '2.0.0-foo-bar-version-1.2.3';
expect(() => assertVersion(settings)).toThrow(/incompatible with Kibana/i);
expect(() => assertVersion(settings)).toThrowErrorMatchingInlineSnapshot(
`"Plugin foo [2.0.0] is incompatible with Kibana [1.0.0]"`
);
});
});

View file

@ -18,7 +18,11 @@
*/
import { analyzeArchive, extractArchive } from './zip';
import validate from 'validate-npm-package-name';
const CAMEL_CASE_REG_EXP = /^[a-z]{1}([a-zA-Z0-9]{1,})$/;
export function isCamelCase(candidate) {
return CAMEL_CASE_REG_EXP.test(candidate);
}
/**
* Checks the plugin name. Will throw an exception if it does not meet
@ -27,9 +31,10 @@ import validate from 'validate-npm-package-name';
* @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`);
if (!isCamelCase(plugin.id)) {
throw new Error(
`Invalid plugin name [${plugin.id}] in kibana.json, expected it to be valid camelCase`
);
}
}
@ -60,17 +65,13 @@ export async function getPackData(settings, logger) {
/**
* Extracts files from a zip archive to a file path using a filter function
*
* @param {string} archive - file path to a zip archive
* @param {string} targetDir - directory path to where the files should
* extracted
*/
export async function extract(settings, logger) {
try {
const plugin = settings.plugins[0];
logger.log('Extracting plugin archive');
await extractArchive(settings.tempArchiveFile, settings.workingPath, plugin.archivePath);
await extractArchive(settings.tempArchiveFile, settings.workingPath, plugin.stripPrefix);
logger.log('Extraction complete');
} catch (err) {
logger.error(err.stack);

View file

@ -18,14 +18,15 @@
*/
import Fs from 'fs';
import { join } from 'path';
import sinon from 'sinon';
import glob from 'glob-all';
import del from 'del';
import Logger from '../lib/logger';
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 () {
@ -73,133 +74,104 @@ describe('kibana cli', function () {
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()).toEqual(expected.sort());
});
// Also only extracts the content from the kibana folder.
// Ignores the others.
it('successfully extract a valid zip', async () => {
await copyReplyFile('test_plugin.zip');
await getPackData(settings, logger);
await extract(settings, logger);
expect(glob.sync('**/*', { cwd: testWorkingPath })).toMatchInlineSnapshot(`
Array [
"archive.part",
"bin",
"bin/executable",
"bin/not-executable",
"kibana.json",
"node_modules",
"node_modules/some-package",
"node_modules/some-package/index.js",
"node_modules/some-package/package.json",
"public",
"public/index.js",
]
`);
});
});
describe('getPackData', function () {
it('populate settings.plugins', function () {
return copyReplyFile('test_plugin.zip')
.then(() => {
return getPackData(settings, logger);
})
.then(() => {
expect(settings.plugins[0].name).toBe('test-plugin');
expect(settings.plugins[0].archivePath).toBe('kibana/test-plugin');
expect(settings.plugins[0].version).toBe('1.0.0');
expect(settings.plugins[0].kibanaVersion).toBe('1.0.0');
});
describe('getPackData', () => {
it('populate settings.plugins', async () => {
await copyReplyFile('test_plugin.zip');
await getPackData(settings, logger);
expect(settings.plugins).toMatchInlineSnapshot(`
Array [
Object {
"id": "testPlugin",
"kibanaVersion": "1.0.0",
"stripPrefix": "kibana/test-plugin",
},
]
`);
});
it('populate settings.plugin.kibanaVersion', function () {
//kibana.version is defined in this package.json and is different than plugin version
return copyReplyFile('test_plugin_different_version.zip')
.then(() => {
return getPackData(settings, logger);
})
.then(() => {
expect(settings.plugins[0].kibanaVersion).toBe('5.0.1');
});
it('populate settings.plugin.kibanaVersion', async () => {
await copyReplyFile('test_plugin_different_version.zip');
await getPackData(settings, logger);
expect(settings.plugins).toMatchInlineSnapshot(`
Array [
Object {
"id": "testPlugin",
"kibanaVersion": "5.0.1",
"stripPrefix": "kibana/test-plugin",
},
]
`);
});
it('populate settings.plugin.kibanaVersion (default to plugin version)', function () {
//kibana.version is not defined in this package.json, defaults to plugin version
return copyReplyFile('test_plugin.zip')
.then(() => {
return getPackData(settings, logger);
})
.then(() => {
expect(settings.plugins[0].kibanaVersion).toBe('1.0.0');
});
it('populate settings.plugins with multiple plugins', async () => {
await copyReplyFile('test_plugin_many.zip');
await getPackData(settings, logger);
expect(settings.plugins).toMatchInlineSnapshot(`
Array [
Object {
"id": "fungerPlugin",
"kibanaVersion": "1.0.0",
"stripPrefix": "kibana/funger-plugin",
},
Object {
"id": "pdf",
"kibanaVersion": "1.0.0",
"stripPrefix": "kibana/pdf",
},
Object {
"id": "testPlugin",
"kibanaVersion": "1.0.0",
"stripPrefix": "kibana/test-plugin",
},
]
`);
});
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).toBe('funger-plugin');
expect(settings.plugins[0].archivePath).toBe('kibana/funger-plugin');
expect(settings.plugins[0].version).toBe('1.0.0');
expect(settings.plugins[1].name).toBe('pdf');
expect(settings.plugins[1].archivePath).toBe('kibana/pdf-linux');
expect(settings.plugins[1].version).toBe('1.0.0');
expect(settings.plugins[2].name).toBe('pdf');
expect(settings.plugins[2].archivePath).toBe('kibana/pdf-win32');
expect(settings.plugins[2].version).toBe('1.0.0');
expect(settings.plugins[3].name).toBe('pdf');
expect(settings.plugins[3].archivePath).toBe('kibana/pdf-win64');
expect(settings.plugins[3].version).toBe('1.0.0');
expect(settings.plugins[4].name).toBe('pdf');
expect(settings.plugins[4].archivePath).toBe('kibana/pdf');
expect(settings.plugins[4].version).toBe('1.0.0');
expect(settings.plugins[5].name).toBe('test-plugin');
expect(settings.plugins[5].archivePath).toBe('kibana/test-plugin');
expect(settings.plugins[5].version).toBe('1.0.0');
});
it('throw an error if there is no kibana plugin', async () => {
await copyReplyFile('test_plugin_no_kibana.zip');
await expect(getPackData(settings, logger)).rejects.toThrowErrorMatchingInlineSnapshot(
`"No kibana plugins found in archive"`
);
});
it('throw an error if there is no kibana plugin', function () {
return copyReplyFile('test_plugin_no_kibana.zip')
.then(() => {
return getPackData(settings, logger);
})
.then(shouldReject, (err) => {
expect(err.message).toMatch(/No kibana plugins found in archive/i);
});
it('throw an error with a corrupt zip', async () => {
await copyReplyFile('corrupt.zip');
await expect(getPackData(settings, logger)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Error retrieving metadata from plugin archive"`
);
});
it('throw an error with a corrupt zip', function () {
return copyReplyFile('corrupt.zip')
.then(() => {
return getPackData(settings, logger);
})
.then(shouldReject, (err) => {
expect(err.message).toMatch(/error retrieving/i);
});
});
it('throw an error if there an invalid plugin name', function () {
return copyReplyFile('invalid_name.zip')
.then(() => {
return getPackData(settings, logger);
})
.then(shouldReject, (err) => {
expect(err.message).toMatch(/invalid plugin name/i);
});
it('throw an error if there an invalid plugin name', async () => {
await copyReplyFile('invalid_name.zip');
await expect(getPackData(settings, logger)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Invalid plugin name [invalid name] in kibana.json, expected it to be valid camelCase"`
);
});
});
});

View file

@ -20,7 +20,7 @@
/**
* Generates file transfer progress messages
*/
export default class Progress {
export class Progress {
constructor(logger) {
const self = this;

View file

@ -18,8 +18,9 @@
*/
import sinon from 'sinon';
import Progress from './progress';
import Logger from '../lib/logger';
import { Progress } from './progress';
import { Logger } from '../lib/logger';
describe('kibana cli', function () {
describe('plugin installer', function () {

View file

@ -18,6 +18,7 @@
*/
import { rename } from 'fs';
import { delay } from 'lodash';
export function renamePlugin(workingPath, finalPath) {
@ -31,8 +32,12 @@ export function renamePlugin(workingPath, finalPath) {
// Retry for up to retryTime seconds
const windowsEPERM = process.platform === 'win32' && err.code === 'EPERM';
const retryAvailable = Date.now() - start < retryTime;
if (windowsEPERM && retryAvailable)
return delay(rename, retryDelay, workingPath, finalPath, retry);
if (windowsEPERM && retryAvailable) {
delay(rename, retryDelay, workingPath, finalPath, retry);
return;
}
reject(err);
}
resolve();

View file

@ -17,9 +17,10 @@
* under the License.
*/
import sinon from 'sinon';
import fs from 'fs';
import sinon from 'sinon';
import { renamePlugin } from './rename';
describe('plugin folder rename', function () {

View file

@ -17,9 +17,12 @@
* under the License.
*/
import expiry from 'expiry-js';
import { resolve } from 'path';
import expiry from 'expiry-js';
import { fromRoot } from '../../core/server/utils';
function generateUrls({ version, plugin }) {
return [
plugin,
@ -46,20 +49,14 @@ export function parse(command, options, kbnPackage) {
quiet: options.quiet || false,
silent: options.silent || false,
config: options.config || '',
optimize: options.optimize,
plugin: command,
version: kbnPackage.version,
pluginDir: options.pluginDir || '',
pluginDir: fromRoot('plugins'),
};
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

@ -17,199 +17,82 @@
* under the License.
*/
import { createAbsolutePathSerializer } from '@kbn/dev-utils';
import { fromRoot } from '../../core/server/utils';
import { resolve } from 'path';
import { parseMilliseconds, parse } 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);
const SECOND = 1000;
const MINUTE = SECOND * 60;
expect(result).toBe(0);
});
expect.addSnapshotSerializer(createAbsolutePathSerializer());
it('should return 0 for a number with an invalid unit of measure', function () {
const result = parseMilliseconds('1gigablasts');
expect(result).toBe(0);
});
it('should assume a number with no unit of measure is specified as milliseconds', function () {
const result = parseMilliseconds(1);
expect(result).toBe(1);
const result2 = parseMilliseconds('1');
expect(result2).toBe(1);
});
it('should interpret a number with "s" as the unit of measure as seconds', function () {
const result = parseMilliseconds('5s');
expect(result).toBe(5 * 1000);
});
it('should interpret a number with "second" as the unit of measure as seconds', function () {
const result = parseMilliseconds('5second');
expect(result).toBe(5 * 1000);
});
it('should interpret a number with "seconds" as the unit of measure as seconds', function () {
const result = parseMilliseconds('5seconds');
expect(result).toBe(5 * 1000);
});
it('should interpret a number with "m" as the unit of measure as minutes', function () {
const result = parseMilliseconds('9m');
expect(result).toBe(9 * 1000 * 60);
});
it('should interpret a number with "minute" as the unit of measure as minutes', function () {
const result = parseMilliseconds('9minute');
expect(result).toBe(9 * 1000 * 60);
});
it('should interpret a number with "minutes" as the unit of measure as minutes', function () {
const result = parseMilliseconds('9minutes');
expect(result).toBe(9 * 1000 * 60);
});
});
describe('parse function', function () {
const command = 'plugin name';
let options = {};
const kbnPackage = { version: 1234 };
beforeEach(function () {
options = { pluginDir: fromRoot('plugins') };
});
describe('timeout option', function () {
it('should default to 0 (milliseconds)', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.timeout).toBe(0);
});
it('should set settings.timeout property', function () {
options.timeout = 1234;
const settings = parse(command, options, kbnPackage);
expect(settings.timeout).toBe(1234);
});
});
describe('quiet option', function () {
it('should default to false', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.quiet).toBe(false);
});
it('should set settings.quiet property to true', function () {
options.quiet = true;
const settings = parse(command, options, kbnPackage);
expect(settings.quiet).toBe(true);
});
});
describe('silent option', function () {
it('should default to false', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.silent).toBe(false);
});
it('should set settings.silent property to true', function () {
options.silent = true;
const settings = parse(command, options, kbnPackage);
expect(settings.silent).toBe(true);
});
});
describe('config option', function () {
it('should default to ZLS', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.config).toBe('');
});
it('should set settings.config property', function () {
options.config = 'foo bar baz';
const settings = parse(command, options, kbnPackage);
expect(settings.config).toBe('foo bar baz');
});
});
describe('pluginDir option', function () {
it('should default to plugins', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.pluginDir).toBe(fromRoot('plugins'));
});
it('should set settings.config property', function () {
options.pluginDir = 'foo bar baz';
const settings = parse(command, options, kbnPackage);
expect(settings.pluginDir).toBe('foo bar baz');
});
});
describe('command value', function () {
it('should set settings.plugin property', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.plugin).toBe(command);
});
});
describe('urls collection', function () {
it('should populate the settings.urls property', function () {
const settings = parse(command, options, kbnPackage);
const expected = [
command,
`https://artifacts.elastic.co/downloads/kibana-plugins/${command}/${command}-1234.zip`,
];
expect(settings.urls).toEqual(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).toBe(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).toBe(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).toBe(expected);
});
});
});
});
describe('parseMilliseconds function', function () {
it.each([
['', 0],
['1gigablasts', 0],
[1, 1],
['1', 1],
['5s', 5 * SECOND],
['5second', 5 * SECOND],
['5seconds', 5 * SECOND],
['9m', 9 * MINUTE],
['9minute', 9 * MINUTE],
['9minutes', 9 * MINUTE],
])('should parse %j to %j', (input, result) => {
expect(parseMilliseconds(input)).toBe(result);
});
});
describe('parse function', function () {
const command = 'plugin name';
const defaultOptions = { pluginDir: fromRoot('plugins') };
const kbnPackage = { version: 1234 };
it('produces expected defaults', function () {
expect(parse(command, { ...defaultOptions }, kbnPackage)).toMatchInlineSnapshot(`
Object {
"config": "",
"plugin": "plugin name",
"pluginDir": <absolute path>/plugins,
"quiet": false,
"silent": false,
"tempArchiveFile": <absolute path>/plugins/.plugin.installing/archive.part,
"timeout": 0,
"urls": Array [
"plugin name",
"https://artifacts.elastic.co/downloads/kibana-plugins/plugin name/plugin name-1234.zip",
],
"version": 1234,
"workingPath": <absolute path>/plugins/.plugin.installing,
}
`);
});
it('consumes overrides', function () {
const options = {
quiet: true,
silent: true,
config: 'foo bar baz',
...defaultOptions,
};
expect(parse(command, options, kbnPackage)).toMatchInlineSnapshot(`
Object {
"config": "foo bar baz",
"plugin": "plugin name",
"pluginDir": <absolute path>/plugins,
"quiet": true,
"silent": true,
"tempArchiveFile": <absolute path>/plugins/.plugin.installing/archive.part,
"timeout": 0,
"urls": Array [
"plugin name",
"https://artifacts.elastic.co/downloads/kibana-plugins/plugin name/plugin name-1234.zip",
],
"version": 1234,
"workingPath": <absolute path>/plugins/.plugin.installing,
}
`);
});
});

View file

@ -17,21 +17,24 @@
* under the License.
*/
import yauzl from 'yauzl';
import path from 'path';
import { createWriteStream, mkdir } from 'fs';
import { get } from 'lodash';
import yauzl from 'yauzl';
const isDirectoryRegex = /(\/|\\)$/;
function isDirectory(filename) {
return isDirectoryRegex.test(filename);
}
/**
* Returns an array of package objects. There will be one for each of
* package.json files in the archive
*
* @param {string} archive - path to plugin archive zip file
* package.json files in the archive
*/
export function analyzeArchive(archive) {
const plugins = [];
const regExp = new RegExp('(kibana[\\\\/][^\\\\/]+)[\\\\/]package.json', 'i');
const regExp = new RegExp('(kibana[\\\\/][^\\\\/]+)[\\\\/]kibana.json', 'i');
return new Promise((resolve, reject) => {
yauzl.open(archive, { lazyEntries: true }, function (err, zipfile) {
@ -47,31 +50,32 @@ export function analyzeArchive(archive) {
return zipfile.readEntry();
}
zipfile.openReadStream(entry, function (err, readable) {
zipfile.openReadStream(entry, function (error, readable) {
const chunks = [];
if (err) {
return reject(err);
if (error) {
return reject(error);
}
readable.on('data', (chunk) => chunks.push(chunk));
readable.on('end', function () {
const contents = Buffer.concat(chunks).toString();
const pkg = JSON.parse(contents);
const manifestJson = Buffer.concat(chunks).toString();
const manifest = JSON.parse(manifestJson);
plugins.push(
Object.assign(pkg, {
archivePath: match[1],
archive: archive,
plugins.push({
id: manifest.id,
stripPrefix: match[1],
// Plugins must specify their version, and by default that version should match
// the version of kibana down to the patch level. If these two versions need
// to diverge, they can specify a kibana.version to indicate the version of
// kibana the plugin is intended to work with.
kibanaVersion: get(pkg, 'kibana.version', pkg.version),
})
);
// Plugins must specify their version, and by default that version in the plugin
// manifest should match the version of kibana down to the patch level. If these
// two versions need plugins can specify a kibanaVersion to indicate the version
// of kibana the plugin is intended to work with.
kibanaVersion:
typeof manifest.kibanaVersion === 'string' && manifest.kibanaVersion
? manifest.kibanaVersion
: manifest.version,
});
zipfile.readEntry();
});
@ -85,12 +89,7 @@ export function analyzeArchive(archive) {
});
}
const isDirectoryRegex = /(\/|\\)$/;
export function _isDirectory(filename) {
return isDirectoryRegex.test(filename);
}
export function extractArchive(archive, targetDir, extractPath) {
export function extractArchive(archive, targetDir, stripPrefix) {
return new Promise((resolve, reject) => {
yauzl.open(archive, { lazyEntries: true }, function (err, zipfile) {
if (err) {
@ -102,8 +101,8 @@ export function extractArchive(archive, targetDir, extractPath) {
zipfile.on('entry', function (entry) {
let fileName = entry.fileName;
if (extractPath && fileName.startsWith(extractPath)) {
fileName = fileName.substring(extractPath.length);
if (stripPrefix && fileName.startsWith(stripPrefix)) {
fileName = fileName.substring(stripPrefix.length);
} else {
return zipfile.readEntry();
}
@ -112,30 +111,34 @@ export function extractArchive(archive, targetDir, extractPath) {
fileName = path.join(targetDir, fileName);
}
if (_isDirectory(fileName)) {
mkdir(fileName, { recursive: true }, function (err) {
if (err) {
return reject(err);
if (isDirectory(fileName)) {
mkdir(fileName, { recursive: true }, function (error) {
if (error) {
return reject(error);
}
zipfile.readEntry();
});
} else {
// file entry
zipfile.openReadStream(entry, function (err, readStream) {
if (err) {
return reject(err);
zipfile.openReadStream(entry, function (error, readStream) {
if (error) {
return reject(error);
}
// ensure parent directory exists
mkdir(path.dirname(fileName), { recursive: true }, function (err) {
if (err) {
return reject(err);
mkdir(path.dirname(fileName), { recursive: true }, function (error2) {
if (error2) {
return reject(error2);
}
readStream.pipe(
createWriteStream(fileName, { mode: entry.externalFileAttributes >>> 16 })
createWriteStream(fileName, {
// eslint-disable-next-line no-bitwise
mode: entry.externalFileAttributes >>> 16,
})
);
readStream.on('end', function () {
zipfile.readEntry();
});

View file

@ -17,12 +17,16 @@
* under the License.
*/
import del from 'del';
import path from 'path';
import os from 'os';
import glob from 'glob';
import fs from 'fs';
import { analyzeArchive, extractArchive, _isDirectory } from './zip';
import del from 'del';
import glob from 'glob';
import { analyzeArchive, extractArchive } from './zip';
const getMode = (path) => (fs.statSync(path).mode & parseInt('777', 8)).toString(8);
describe('kibana cli', function () {
describe('zip', function () {
@ -43,32 +47,37 @@ describe('kibana cli', function () {
describe('analyzeArchive', function () {
it('returns array of plugins', async () => {
const packages = await analyzeArchive(archivePath);
const plugin = packages[0];
expect(packages).toBeInstanceOf(Array);
expect(plugin.name).toBe('test-plugin');
expect(plugin.archivePath).toBe('kibana/test-plugin');
expect(plugin.archive).toBe(archivePath);
expect(plugin.kibanaVersion).toBe('1.0.0');
expect(packages).toMatchInlineSnapshot(`
Array [
Object {
"id": "testPlugin",
"kibanaVersion": "1.0.0",
"stripPrefix": "kibana/test-plugin",
},
]
`);
});
});
describe('extractArchive', () => {
it('extracts files using the extractPath filter', async () => {
const archive = path.resolve(repliesPath, 'test_plugin_many.zip');
const archive = path.resolve(repliesPath, 'test_plugin.zip');
await extractArchive(archive, tempPath, 'kibana/test-plugin');
const files = await glob.sync('**/*', { cwd: tempPath });
const expected = [
'extra file only in zip.txt',
'index.js',
'package.json',
'public',
'public/app.js',
'README.md',
];
expect(files.sort()).toEqual(expected.sort());
expect(glob.sync('**/*', { cwd: tempPath })).toMatchInlineSnapshot(`
Array [
"bin",
"bin/executable",
"bin/not-executable",
"kibana.json",
"node_modules",
"node_modules/some-package",
"node_modules/some-package/index.js",
"node_modules/some-package/package.json",
"public",
"public/index.js",
]
`);
});
});
@ -76,49 +85,26 @@ describe('kibana cli', function () {
it('verify consistency of modes of files', async () => {
const archivePath = path.resolve(repliesPath, 'test_plugin.zip');
await extractArchive(archivePath, tempPath, 'kibana/libs');
const files = await glob.sync('**/*', { cwd: tempPath });
await extractArchive(archivePath, tempPath, 'kibana/test-plugin/bin');
const expected = ['executable', 'unexecutable'];
expect(files.sort()).toEqual(expected.sort());
expect(glob.sync('**/*', { cwd: tempPath })).toMatchInlineSnapshot(`
Array [
"executable",
"not-executable",
]
`);
const executableMode =
'0' +
(fs.statSync(path.resolve(tempPath, 'executable')).mode & parseInt('777', 8)).toString(8);
const unExecutableMode =
'0' +
(fs.statSync(path.resolve(tempPath, 'unexecutable')).mode & parseInt('777', 8)).toString(
8
);
expect(executableMode).toEqual('0755');
expect(unExecutableMode).toEqual('0644');
expect(getMode(path.resolve(tempPath, 'executable'))).toEqual('755');
expect(getMode(path.resolve(tempPath, 'not-executable'))).toEqual('644');
});
});
it('handles a corrupt zip archive', async () => {
try {
await extractArchive(path.resolve(repliesPath, 'corrupt.zip'));
throw new Error('This should have failed');
} catch (e) {
return;
}
});
});
describe('_isDirectory', () => {
it('should check for a forward slash', () => {
expect(_isDirectory('/foo/bar/')).toBe(true);
});
it('should check for a backslash', () => {
expect(_isDirectory('\\foo\\bar\\')).toBe(true);
});
it('should return false for files', () => {
expect(_isDirectory('foo.txt')).toBe(false);
expect(_isDirectory('\\path\\to\\foo.txt')).toBe(false);
expect(_isDirectory('/path/to/foo.txt')).toBe(false);
await expect(
extractArchive(path.resolve(repliesPath, 'corrupt.zip'))
).rejects.toThrowErrorMatchingInlineSnapshot(
`"end of central directory record signature not found"`
);
});
});
});

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { isOSS } from './is_oss';
import { isOss } from './is_oss';
function isXPack(plugin) {
return /x-pack/.test(plugin);
@ -25,7 +25,7 @@ function isXPack(plugin) {
export function errorIfXPackInstall(settings) {
if (isXPack(settings.plugin)) {
if (isOSS()) {
if (isOss()) {
throw new Error(
'You are using the OSS-only distribution of Kibana. ' +
'As of version 6.3+ X-Pack is bundled in the standard distribution of this software by default; ' +
@ -40,7 +40,7 @@ export function errorIfXPackInstall(settings) {
}
export function errorIfXPackRemove(settings) {
if (isXPack(settings.plugin) && !isOSS()) {
if (isXPack(settings.plugin) && !isOss()) {
throw new Error(
'You are using the standard distribution of Kibana. Please install the OSS-only distribution to remove X-Pack features.'
);

View file

@ -20,6 +20,6 @@
import fs from 'fs';
import path from 'path';
export function isOSS() {
export function isOss() {
return !fs.existsSync(path.resolve(__dirname, '../../../x-pack'));
}

View file

@ -17,12 +17,12 @@
* under the License.
*/
import { isOSS } from './is_oss';
import { isOss } from './is_oss';
describe('is_oss', () => {
describe('x-pack installed', () => {
it('should return false', () => {
expect(isOSS()).toEqual(false);
expect(isOss()).toEqual(false);
});
});
});

View file

@ -17,7 +17,7 @@
* under the License.
*/
export default function (settings, logger) {
export function logWarnings(logger) {
process.on('warning', (warning) => {
// deprecation warnings do no reflect a current problem for
// the user and therefor should be filtered out.

View file

@ -20,7 +20,7 @@
/**
* Logs messages and errors
*/
export default class Logger {
export class Logger {
constructor(settings = {}) {
this.previousLineEnded = true;
this.silent = !!settings.silent;

View file

@ -18,7 +18,8 @@
*/
import sinon from 'sinon';
import Logger from './logger';
import { Logger } from './logger';
describe('kibana cli', function () {
describe('plugin installer', function () {

View file

@ -1,27 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export function warnIfUsingPluginDirOption(options, defaultValue, logger) {
if (options && options.pluginDir !== defaultValue) {
logger.log(
'Warning: Using the -d, --plugin-dir option is deprecated, and is ' +
'known to not work for all plugins, including X-Pack.'
);
}
}

View file

@ -18,37 +18,16 @@
*/
import { fromRoot } from '../../core/server/utils';
import list from './list';
import Logger from '../lib/logger';
import { parse } from './settings';
import logWarnings from '../lib/log_warnings';
import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option';
import { list } from './list';
import { Logger } from '../lib/logger';
import { logWarnings } from '../lib/log_warnings';
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);
warnIfUsingPluginDirOption(settings, fromRoot('plugins'), logger);
logWarnings(settings, logger);
list(settings, logger);
function processCommand() {
const logger = new Logger();
logWarnings(logger);
list(fromRoot('plugins'), logger);
}
export default function pluginList(program) {
program
.command('list')
.option(
'-d, --plugin-dir <path>',
'path to the directory where plugins are stored (DEPRECATED, known to not work for all plugins)',
fromRoot('plugins')
)
.description('list installed plugins')
.action(processCommand);
export function listCommand(program) {
program.command('list').description('list installed plugins').action(processCommand);
}

View file

@ -20,19 +20,20 @@
import { statSync, readdirSync, readFileSync } from 'fs';
import { join } from 'path';
export default function list(settings, logger) {
readdirSync(settings.pluginDir).forEach((filename) => {
const stat = statSync(join(settings.pluginDir, filename));
export function list(pluginDir, logger) {
readdirSync(pluginDir).forEach((name) => {
const stat = statSync(join(pluginDir, name));
if (stat.isDirectory() && filename[0] !== '.') {
if (stat.isDirectory() && name[0] !== '.') {
try {
const packagePath = join(settings.pluginDir, filename, 'package.json');
const { version } = JSON.parse(readFileSync(packagePath, 'utf8'));
logger.log(filename + '@' + version);
const packagePath = join(pluginDir, name, 'kibana.json');
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
logger.log(pkg.id + '@' + pkg.version);
} catch (e) {
throw new Error('Unable to read package.json file for plugin ' + filename);
throw new Error('Unable to read kibana.json file for plugin ' + name);
}
}
});
logger.log(''); //intentional blank line for aesthetics
}

View file

@ -17,78 +17,67 @@
* under the License.
*/
import sinon from 'sinon';
import del from 'del';
import Logger from '../lib/logger';
import list from './list';
import { join } from 'path';
import { writeFileSync, appendFileSync, mkdirSync } from 'fs';
import { writeFileSync, mkdirSync } from 'fs';
import del from 'del';
import { list } from './list';
function createPlugin(name, version, pluginBaseDir) {
const pluginDir = join(pluginBaseDir, name);
mkdirSync(pluginDir, { recursive: true });
appendFileSync(join(pluginDir, 'package.json'), '{"version": "' + version + '"}');
writeFileSync(
join(pluginDir, 'kibana.json'),
JSON.stringify({
id: name,
version,
})
);
}
const logger = {
messages: [],
log(msg) {
this.messages.push(`log: ${msg}`);
},
error(msg) {
this.messages.push(`error: ${msg}`);
},
};
describe('kibana cli', function () {
describe('plugin lister', function () {
const pluginDir = join(__dirname, '.test.data.list');
let logger;
const settings = {
pluginDir: pluginDir,
};
beforeEach(function () {
logger = new Logger(settings);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
logger.messages.length = 0;
del.sync(pluginDir);
mkdirSync(pluginDir, { recursive: true });
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
del.sync(pluginDir);
});
it('list all of the folders in the plugin folder', function () {
createPlugin('plugin1', '5.0.0-alpha2', pluginDir);
createPlugin('plugin2', '3.2.1', pluginDir);
createPlugin('plugin3', '1.2.3', pluginDir);
list(settings, logger);
expect(logger.log.calledWith('plugin1@5.0.0-alpha2')).toBe(true);
expect(logger.log.calledWith('plugin2@3.2.1')).toBe(true);
expect(logger.log.calledWith('plugin3@1.2.3')).toBe(true);
});
it('ignore folders that start with a period', function () {
it('list all of the folders in the plugin folder, ignoring dot prefixed plugins and regular files', function () {
createPlugin('.foo', '1.0.0', pluginDir);
createPlugin('plugin1', '5.0.0-alpha2', pluginDir);
createPlugin('plugin2', '3.2.1', pluginDir);
createPlugin('plugin3', '1.2.3', pluginDir);
createPlugin('.bar', '1.0.0', pluginDir);
list(settings, logger);
expect(logger.log.calledWith('.foo@1.0.0')).toBe(false);
expect(logger.log.calledWith('.bar@1.0.0')).toBe(false);
});
it('list should only list folders', function () {
createPlugin('plugin1', '1.0.0', pluginDir);
createPlugin('plugin2', '1.0.0', pluginDir);
createPlugin('plugin3', '1.0.0', pluginDir);
writeFileSync(join(pluginDir, 'plugin4'), 'This is a file, and not a folder.');
list(settings, logger);
list(pluginDir, logger);
expect(logger.log.calledWith('plugin1@1.0.0')).toBe(true);
expect(logger.log.calledWith('plugin2@1.0.0')).toBe(true);
expect(logger.log.calledWith('plugin3@1.0.0')).toBe(true);
expect(logger.messages).toMatchInlineSnapshot(`
Array [
"log: plugin1@5.0.0-alpha2",
"log: plugin2@3.2.1",
"log: plugin3@1.2.3",
"log: ",
]
`);
});
it('list should throw an exception if a plugin does not have a package.json', function () {
@ -96,19 +85,23 @@ describe('kibana cli', function () {
mkdirSync(join(pluginDir, 'empty-plugin'), { recursive: true });
expect(function () {
list(settings, logger);
}).toThrowError('Unable to read package.json file for plugin empty-plugin');
list(pluginDir, logger);
}).toThrowErrorMatchingInlineSnapshot(
`"Unable to read kibana.json file for plugin empty-plugin"`
);
});
it('list should throw an exception if a plugin have an empty package.json', function () {
createPlugin('plugin1', '1.0.0', pluginDir);
const invalidPluginDir = join(pluginDir, 'invalid-plugin');
mkdirSync(invalidPluginDir, { recursive: true });
appendFileSync(join(invalidPluginDir, 'package.json'), '');
writeFileSync(join(invalidPluginDir, 'package.json'), '');
expect(function () {
list(settings, logger);
}).toThrowError('Unable to read package.json file for plugin invalid-plugin');
list(pluginDir, logger);
}).toThrowErrorMatchingInlineSnapshot(
`"Unable to read kibana.json file for plugin invalid-plugin"`
);
});
});
});

View file

@ -1,26 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export function parse(command) {
const settings = {
pluginDir: command.pluginDir || '',
};
return settings;
}

View file

@ -1,50 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { fromRoot } from '../../core/server/utils';
import { 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('plugins') };
});
describe('pluginDir option', function () {
it('should default to plugins', function () {
const settings = parse(command, options);
expect(settings.pluginDir).toBe(fromRoot('plugins'));
});
it('should set settings.config property', function () {
command.pluginDir = 'foo bar baz';
const settings = parse(command, options);
expect(settings.pluginDir).toBe('foo bar baz');
});
});
});
});
});
});

View file

@ -17,46 +17,34 @@
* under the License.
*/
import { fromRoot } from '../../core/server/utils';
import remove from './remove';
import Logger from '../lib/logger';
import { remove } from './remove';
import { Logger } from '../lib/logger';
import { parse } from './settings';
import { getConfigPath } from '../../core/server/path';
import logWarnings from '../lib/log_warnings';
import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option';
import { logWarnings } from '../lib/log_warnings';
function processCommand(command, options) {
let settings;
try {
settings = parse(command, options);
} catch (ex) {
//The logger has not yet been initialized.
// 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);
warnIfUsingPluginDirOption(settings, fromRoot('plugins'), logger);
logWarnings(settings, logger);
remove(settings, logger);
}
export default function pluginRemove(program) {
export function removeCommand(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', getConfigPath())
.option(
'-d, --plugin-dir <path>',
'path to the directory where plugins are stored (DEPRECATED, known to not work for all plugins)',
fromRoot('plugins')
)
.description(
'remove a plugin',
`common examples:
remove x-pack`
)
.description('remove a plugin')
.action(processCommand);
}

View file

@ -18,11 +18,12 @@
*/
import { statSync } from 'fs';
import { errorIfXPackRemove } from '../lib/error_if_x_pack';
import del from 'del';
export default function remove(settings, logger) {
import { errorIfXPackRemove } from '../lib/error_if_x_pack';
export function remove(settings, logger) {
try {
let stat;
try {

View file

@ -17,13 +17,15 @@
* under the License.
*/
import { join } from 'path';
import { writeFileSync, existsSync, mkdirSync } from 'fs';
import sinon from 'sinon';
import glob from 'glob-all';
import del from 'del';
import Logger from '../lib/logger';
import remove from './remove';
import { join } from 'path';
import { writeFileSync, existsSync, mkdirSync } from 'fs';
import { Logger } from '../lib/logger';
import { remove } from './remove';
describe('kibana cli', function () {
describe('plugin remover', function () {

View file

@ -19,12 +19,14 @@
import { resolve } from 'path';
import { fromRoot } from '../../core/server/utils';
export function parse(command, options) {
const settings = {
quiet: options.quiet || false,
silent: options.silent || false,
config: options.config || '',
pluginDir: options.pluginDir || '',
pluginDir: fromRoot('plugins'),
plugin: command,
};

View file

@ -17,88 +17,42 @@
* under the License.
*/
import { fromRoot } from '../../core/server/utils';
import { createAbsolutePathSerializer } from '@kbn/dev-utils';
import { 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('plugins') };
});
const command = 'plugin name';
describe('quiet option', function () {
it('should default to false', function () {
const settings = parse(command, options, kbnPackage);
expect.addSnapshotSerializer(createAbsolutePathSerializer());
expect(settings.quiet).toBe(false);
});
it('should set settings.quiet property to true', function () {
options.quiet = true;
const settings = parse(command, options, kbnPackage);
expect(settings.quiet).toBe(true);
});
});
describe('silent option', function () {
it('should default to false', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.silent).toBe(false);
});
it('should set settings.silent property to true', function () {
options.silent = true;
const settings = parse(command, options, kbnPackage);
expect(settings.silent).toBe(true);
});
});
describe('config option', function () {
it('should default to ZLS', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.config).toBe('');
});
it('should set settings.config property', function () {
options.config = 'foo bar baz';
const settings = parse(command, options, kbnPackage);
expect(settings.config).toBe('foo bar baz');
});
});
describe('pluginDir option', function () {
it('should default to plugins', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.pluginDir).toBe(fromRoot('plugins'));
});
it('should set settings.config property', function () {
options.pluginDir = 'foo bar baz';
const settings = parse(command, options, kbnPackage);
expect(settings.pluginDir).toBe('foo bar baz');
});
});
describe('command value', function () {
it('should set settings.plugin property', function () {
const settings = parse(command, options, kbnPackage);
expect(settings.plugin).toBe(command);
});
});
});
});
});
it('produces the defaults', () => {
expect(parse(command, {})).toMatchInlineSnapshot(`
Object {
"config": "",
"plugin": "plugin name",
"pluginDir": <absolute path>/plugins,
"pluginPath": <absolute path>/plugins/plugin name,
"quiet": false,
"silent": false,
}
`);
});
it('overrides the defaults with the parsed cli options', () => {
expect(
parse(command, {
quiet: true,
silent: true,
config: 'foo/bar',
})
).toMatchInlineSnapshot(`
Object {
"config": "foo/bar",
"plugin": "plugin name",
"pluginDir": <absolute path>/plugins,
"pluginPath": <absolute path>/plugins/plugin name,
"quiet": true,
"silent": true,
}
`);
});

View file

@ -7878,11 +7878,6 @@ builtin-status-codes@^3.0.0:
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
builtins@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/builtins/-/builtins-0.0.7.tgz#355219cd6cf18dbe7c01cc7fd2dce765cfdc549a"
integrity sha1-NVIZzWzxjb58Acx/0tznZc/cVJo=
bytes@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8"
@ -29959,13 +29954,6 @@ validate-npm-package-license@^3.0.1:
spdx-correct "~1.0.0"
spdx-expression-parse "~1.0.0"
validate-npm-package-name@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-2.2.2.tgz#f65695b22f7324442019a3c7fa39a6e7fd299085"
integrity sha1-9laVsi9zJEQgGaPH+jmm5/0pkIU=
dependencies:
builtins "0.0.7"
validator@^10.11.0:
version "10.11.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"