Making schema async, and plugin discovery expose raw package jsons (#18926) (#18940)

* Making schema async, and plugin discovery expose raw package jsons

* Addressing some peer review comments

* Modifying the way we emit the extendedConfig

* Removing errant config

* Adding filter and last so we only get the last non-null one
This commit is contained in:
Brandon Kobel 2018-05-09 10:39:59 -04:00 committed by GitHub
parent 1fd0f7aae9
commit c9cb8108e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 485 additions and 292 deletions

View file

@ -5,22 +5,17 @@ import { map as promiseMap, fromNode } from 'bluebird';
import { Agent as HttpsAgent } from 'https';
import { readFileSync } from 'fs';
import { Config } from '../../server/config/config';
import { setupConnection } from '../../server/http/setup_connection';
import { registerHapiPlugins } from '../../server/http/register_hapi_plugins';
import { setupLogging } from '../../server/logging';
import { transformDeprecations } from '../../server/config/transform_deprecations';
const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split('');
export default class BasePathProxy {
constructor(clusterManager, userSettings) {
constructor(clusterManager, config) {
this.clusterManager = clusterManager;
this.server = new Server();
const settings = transformDeprecations(userSettings);
const config = Config.withDefaultSchema(settings);
this.targetPort = config.get('dev.basePathProxyTarget');
this.basePath = config.get('server.basePath');

View file

@ -4,11 +4,20 @@ import { debounce, invoke, bindAll, once, uniq } from 'lodash';
import Log from '../log';
import Worker from './worker';
import BasePathProxy from './base_path_proxy';
import { Config } from '../../server/config/config';
import { transformDeprecations } from '../../server/config/transform_deprecations';
process.env.kbnWorkerType = 'managr';
export default class ClusterManager {
constructor(opts = {}, settings = {}) {
static async create(opts = {}, settings = {}) {
const transformedSettings = transformDeprecations(settings);
const config = await Config.withDefaultSchema(transformedSettings);
return new ClusterManager(opts, config);
}
constructor(opts, config) {
this.log = new Log(opts.quiet, opts.silent);
this.addedCount = 0;
@ -19,7 +28,7 @@ export default class ClusterManager {
];
if (opts.basePath) {
this.basePathProxy = new BasePathProxy(this, settings);
this.basePathProxy = new BasePathProxy(this, config);
optimizerArgv.push(
`--server.basePath=${this.basePathProxy.basePath}`,
@ -63,14 +72,16 @@ export default class ClusterManager {
bindAll(this, 'onWatcherAdd', 'onWatcherError', 'onWatcherChange');
if (opts.watch) {
const pluginPaths = config.get('plugins.paths');
const scanDirs = config.get('plugins.scanDirs');
const extraPaths = [
...settings.plugins.paths,
...settings.plugins.scanDirs,
...pluginPaths,
...scanDirs,
];
const extraIgnores = settings.plugins.scanDirs
const extraIgnores = scanDirs
.map(scanDir => resolve(scanDir, '*'))
.concat(settings.plugins.paths)
.concat(pluginPaths)
.reduce((acc, path) => acc.concat(
resolve(path, 'test'),
resolve(path, 'build'),

View file

@ -26,8 +26,8 @@ describe('CLI cluster manager', function () {
sandbox.restore();
});
it('has two workers', function () {
const manager = new ClusterManager({});
it('has two workers', async function () {
const manager = await ClusterManager.create({});
expect(manager.workers).toHaveLength(2);
for (const worker of manager.workers) expect(worker).toBeInstanceOf(Worker);
@ -36,8 +36,8 @@ describe('CLI cluster manager', function () {
expect(manager.server).toBeInstanceOf(Worker);
});
it('delivers broadcast messages to other workers', function () {
const manager = new ClusterManager({});
it('delivers broadcast messages to other workers', async function () {
const manager = await ClusterManager.create({});
for (const worker of manager.workers) {
Worker.prototype.start.call(worker);// bypass the debounced start method

View file

@ -184,7 +184,7 @@ export default function (program) {
if (CAN_CLUSTER && opts.dev && !isWorker) {
// stop processing the action and handoff to cluster manager
const ClusterManager = require(CLUSTER_MANAGER_PATH);
new ClusterManager(opts, settings);
await ClusterManager.create(opts, settings);
return;
}
@ -216,10 +216,10 @@ export default function (program) {
process.exit(exitCode);
}
process.on('SIGHUP', function reloadConfig() {
process.on('SIGHUP', async function reloadConfig() {
const settings = getCurrentSettings();
kbnServer.server.log(['info', 'config'], 'Reloading logging configuration due to SIGHUP.');
kbnServer.applyLoggingConfiguration(settings);
await kbnServer.applyLoggingConfiguration(settings);
kbnServer.server.log(['info', 'config'], 'Reloaded logging configuration due to SIGHUP.');
});

View file

@ -1,6 +1,7 @@
import { resolve } from 'path';
import expect from 'expect.js';
import { isEqual } from 'lodash';
import { findPluginSpecs } from '../find_plugin_specs';
import { PluginSpec } from '../plugin_spec';
@ -11,107 +12,183 @@ describe('plugin discovery', () => {
describe('findPluginSpecs()', function () {
this.timeout(10000);
it('finds specs for specified plugin paths', async () => {
const { spec$ } = findPluginSpecs({
plugins: {
paths: [
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'bar'),
]
}
});
const specs = await spec$.toArray().toPromise();
expect(specs).to.have.length(3);
specs.forEach(spec => {
expect(spec).to.be.a(PluginSpec);
});
expect(specs.map(s => s.getId()).sort())
.to.eql(['bar:one', 'bar:two', 'foo']);
});
it('finds all specs in scanDirs', async () => {
const { spec$ } = findPluginSpecs({
// used to ensure the dev_mode plugin is enabled
env: 'development',
plugins: {
scanDirs: [PLUGIN_FIXTURES]
}
});
const specs = await spec$.toArray().toPromise();
expect(specs).to.have.length(3);
specs.forEach(spec => {
expect(spec).to.be.a(PluginSpec);
});
expect(specs.map(s => s.getId()).sort())
.to.eql(['bar:one', 'bar:two', 'foo']);
});
it('does not find disabled plugins', async () => {
const { spec$ } = findPluginSpecs({
'bar:one': {
enabled: false
},
plugins: {
paths: [
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'bar')
]
}
});
const specs = await spec$.toArray().toPromise();
expect(specs).to.have.length(2);
specs.forEach(spec => {
expect(spec).to.be.a(PluginSpec);
});
expect(specs.map(s => s.getId()).sort())
.to.eql(['bar:two', 'foo']);
});
it('dedupes duplicate packs', async () => {
const { spec$ } = findPluginSpecs({
plugins: {
scanDirs: [PLUGIN_FIXTURES],
paths: [
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'bar'),
resolve(PLUGIN_FIXTURES, 'bar'),
],
}
});
const specs = await spec$.toArray().toPromise();
expect(specs).to.have.length(3);
specs.forEach(spec => {
expect(spec).to.be.a(PluginSpec);
});
expect(specs.map(s => s.getId()).sort())
.to.eql(['bar:one', 'bar:two', 'foo']);
});
describe('conflicting plugin spec ids', () => {
it('fails with informative message', async () => {
describe('spec$', () => {
it('finds specs for specified plugin paths', async () => {
const { spec$ } = findPluginSpecs({
plugins: {
scanDirs: [],
paths: [
resolve(CONFLICT_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'bar'),
resolve(PLUGIN_FIXTURES, 'broken'),
]
}
});
const specs = await spec$.toArray().toPromise();
expect(specs).to.have.length(3);
specs.forEach(spec => {
expect(spec).to.be.a(PluginSpec);
});
expect(specs.map(s => s.getId()).sort())
.to.eql(['bar:one', 'bar:two', 'foo']);
});
it('finds all specs in scanDirs', async () => {
const { spec$ } = findPluginSpecs({
// used to ensure the dev_mode plugin is enabled
env: 'development',
plugins: {
scanDirs: [PLUGIN_FIXTURES]
}
});
const specs = await spec$.toArray().toPromise();
expect(specs).to.have.length(3);
specs.forEach(spec => {
expect(spec).to.be.a(PluginSpec);
});
expect(specs.map(s => s.getId()).sort())
.to.eql(['bar:one', 'bar:two', 'foo']);
});
it('does not find disabled plugins', async () => {
const { spec$ } = findPluginSpecs({
'bar:one': {
enabled: false
},
plugins: {
paths: [
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'bar'),
resolve(PLUGIN_FIXTURES, 'broken'),
]
}
});
const specs = await spec$.toArray().toPromise();
expect(specs).to.have.length(2);
specs.forEach(spec => {
expect(spec).to.be.a(PluginSpec);
});
expect(specs.map(s => s.getId()).sort())
.to.eql(['bar:two', 'foo']);
});
it('dedupes duplicate packs', async () => {
const { spec$ } = findPluginSpecs({
plugins: {
scanDirs: [PLUGIN_FIXTURES],
paths: [
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'bar'),
resolve(PLUGIN_FIXTURES, 'bar'),
resolve(PLUGIN_FIXTURES, 'broken'),
resolve(PLUGIN_FIXTURES, 'broken'),
],
}
});
try {
await spec$.toArray().toPromise();
throw new Error('expected spec$ to throw an error');
} catch (error) {
expect(error.message).to.contain('Multple plugins found with the id "foo"');
expect(error.message).to.contain(CONFLICT_FIXTURES);
}
const specs = await spec$.toArray().toPromise();
expect(specs).to.have.length(3);
specs.forEach(spec => {
expect(spec).to.be.a(PluginSpec);
});
expect(specs.map(s => s.getId()).sort())
.to.eql(['bar:one', 'bar:two', 'foo']);
});
describe('conflicting plugin spec ids', () => {
it('fails with informative message', async () => {
const { spec$ } = findPluginSpecs({
plugins: {
scanDirs: [],
paths: [
resolve(CONFLICT_FIXTURES, 'foo'),
],
}
});
try {
await spec$.toArray().toPromise();
throw new Error('expected spec$ to throw an error');
} catch (error) {
expect(error.message).to.contain('Multple plugins found with the id "foo"');
expect(error.message).to.contain(CONFLICT_FIXTURES);
}
});
});
});
describe('packageJson$', () => {
const checkPackageJsons = (packageJsons) => {
expect(packageJsons).to.have.length(2);
const package1 = packageJsons.find(packageJson => isEqual({
directoryPath: resolve(PLUGIN_FIXTURES, 'foo'),
contents: {
name: 'foo',
version: 'kibana'
}
}, packageJson));
expect(package1).to.be.an(Object);
const package2 = packageJsons.find(packageJson => isEqual({
directoryPath: resolve(PLUGIN_FIXTURES, 'bar'),
contents: {
name: 'foo',
version: 'kibana'
}
}, packageJson));
expect(package2).to.be.an(Object);
};
it('finds packageJson for specified plugin paths', async () => {
const { packageJson$ } = findPluginSpecs({
plugins: {
paths: [
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'bar'),
resolve(PLUGIN_FIXTURES, 'broken'),
]
}
});
const packageJsons = await packageJson$.toArray().toPromise();
checkPackageJsons(packageJsons);
});
it('finds all packageJsons in scanDirs', async () => {
const { packageJson$ } = findPluginSpecs({
// used to ensure the dev_mode plugin is enabled
env: 'development',
plugins: {
scanDirs: [PLUGIN_FIXTURES]
}
});
const packageJsons = await packageJson$.toArray().toPromise();
checkPackageJsons(packageJsons);
});
it('dedupes duplicate packageJson', async () => {
const { packageJson$ } = findPluginSpecs({
plugins: {
scanDirs: [PLUGIN_FIXTURES],
paths: [
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'foo'),
resolve(PLUGIN_FIXTURES, 'bar'),
resolve(PLUGIN_FIXTURES, 'bar'),
resolve(PLUGIN_FIXTURES, 'broken'),
resolve(PLUGIN_FIXTURES, 'broken'),
],
}
});
const packageJsons = await packageJson$.toArray().toPromise();
checkPackageJsons(packageJsons);
});
});
});

View file

@ -0,0 +1,3 @@
export default {
foo: 'bar'
};

View file

@ -0,0 +1,4 @@
{
"name": "baz",
"version": "kibana"
}

View file

@ -9,8 +9,9 @@ import {
} from './plugin_config';
import {
createPackAtPath$,
createPacksInDirectory$,
createPack$,
createPackageJsonAtPath$,
createPackageJsonsInDirectory$,
} from './plugin_pack';
import {
@ -18,8 +19,8 @@ import {
isInvalidPackError,
} from './errors';
function defaultConfig(settings) {
return Config.withDefaultSchema(
async function defaultConfig(settings) {
return await Config.withDefaultSchema(
transformDeprecations(settings)
);
}
@ -45,8 +46,8 @@ function getDistinctKeyForFindResult(result) {
}
// packs are distinct by their absolute and real path
if (result.pack) {
return realpathSync(result.pack.getPath());
if (result.packageJson) {
return realpathSync(result.packageJson.directoryPath);
}
// non error/pack results shouldn't exist, but if they do they are all unique
@ -75,92 +76,114 @@ function groupSpecsById(specs) {
* the config from discovered plugin specs
* @return {Object<name,Rx>}
*/
export function findPluginSpecs(settings, config = defaultConfig(settings)) {
export function findPluginSpecs(settings, configToMutate) {
const config$ = Observable.defer(async () => {
if (configToMutate) {
return configToMutate;
}
return await defaultConfig(settings);
}).shareReplay();
// find plugin packs in configured paths/dirs
const find$ = Observable.merge(
...config.get('plugins.paths').map(createPackAtPath$),
...config.get('plugins.scanDirs').map(createPacksInDirectory$)
)
const packageJson$ = config$.mergeMap(config => {
return Observable.merge(
...config.get('plugins.paths').map(createPackageJsonAtPath$),
...config.get('plugins.scanDirs').map(createPackageJsonsInDirectory$)
);
})
.distinct(getDistinctKeyForFindResult)
.share();
const extendConfig$ = find$
// get the specs for each found plugin pack
.mergeMap(({ pack }) => (
pack ? pack.getPluginSpecs() : []
))
// make sure that none of the plugin specs have conflicting ids, fail
// early if conflicts detected or merge the specs back into the stream
.toArray()
.mergeMap(allSpecs => {
for (const [id, specs] of groupSpecsById(allSpecs)) {
if (specs.length > 1) {
throw new Error(
`Multple plugins found with the id "${id}":\n${
specs.map(spec => ` - ${id} at ${spec.getPath()}`).join('\n')
}`
);
const pack$ = createPack$(packageJson$)
.share();
const extendConfig$ = config$.mergeMap(config => {
return pack$
// get the specs for each found plugin pack
.mergeMap(({ pack }) => (
pack ? pack.getPluginSpecs() : []
))
// make sure that none of the plugin specs have conflicting ids, fail
// early if conflicts detected or merge the specs back into the stream
.toArray()
.mergeMap(allSpecs => {
for (const [id, specs] of groupSpecsById(allSpecs)) {
if (specs.length > 1) {
throw new Error(
`Multple plugins found with the id "${id}":\n${
specs.map(spec => ` - ${id} at ${spec.getPath()}`).join('\n')
}`
);
}
}
}
return allSpecs;
})
.mergeMap(async (spec) => {
// extend the config service with this plugin spec and
// collect its deprecations messages if some of its
// settings are outdated
const deprecations = [];
await extendConfigService(spec, config, settings, (message) => {
deprecations.push({ spec, message });
return allSpecs;
})
.mergeMap(async (spec) => {
// extend the config service with this plugin spec and
// collect its deprecations messages if some of its
// settings are outdated
const deprecations = [];
await extendConfigService(spec, config, settings, (message) => {
deprecations.push({ spec, message });
});
return {
spec,
deprecations,
};
})
// extend the config with all plugins before determining enabled status
.let(bufferAllResults)
.map(({ spec, deprecations }) => {
const isRightVersion = spec.isVersionCompatible(config.get('pkg.version'));
const enabled = isRightVersion && spec.isEnabled(config);
return {
config,
spec,
deprecations,
enabledSpecs: enabled ? [spec] : [],
disabledSpecs: enabled ? [] : [spec],
invalidVersionSpecs: isRightVersion ? [] : [spec],
};
})
// determine which plugins are disabled before actually removing things from the config
.let(bufferAllResults)
.do(result => {
for (const spec of result.disabledSpecs) {
disableConfigExtension(spec, config);
}
});
return {
spec,
deprecations,
};
})
// extend the config with all plugins before determining enabled status
.let(bufferAllResults)
.map(({ spec, deprecations }) => {
const isRightVersion = spec.isVersionCompatible(config.get('pkg.version'));
const enabled = isRightVersion && spec.isEnabled(config);
return {
spec,
deprecations,
enabledSpecs: enabled ? [spec] : [],
disabledSpecs: enabled ? [] : [spec],
invalidVersionSpecs: isRightVersion ? [] : [spec],
};
})
// determine which plugins are disabled before actually removing things from the config
.let(bufferAllResults)
.do(result => {
for (const spec of result.disabledSpecs) {
disableConfigExtension(spec, config);
}
})
})
.share();
return {
// package JSONs found when searching configure paths
packageJson$: packageJson$
.mergeMap(result => (
result.packageJson ? [result.packageJson] : []
)),
// plugin packs found when searching configured paths
pack$: find$
pack$: pack$
.mergeMap(result => (
result.pack ? [result.pack] : []
)),
// errors caused by invalid directories of plugin directories
invalidDirectoryError$: find$
invalidDirectoryError$: pack$
.mergeMap(result => (
isInvalidDirectoryError(result.error) ? [result.error] : []
)),
// errors caused by directories that we expected to be plugin but were invalid
invalidPackError$: find$
invalidPackError$: pack$
.mergeMap(result => (
isInvalidPackError(result.error) ? [result.error] : []
)),
otherError$: find$
otherError$: pack$
.mergeMap(result => (
isUnhandledError(result.error) ? [result.error] : []
)),
@ -173,8 +196,9 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) {
// the config service we extended with all of the plugin specs,
// only emitted once it is fully extended by all
extendedConfig$: extendConfig$
.ignoreElements()
.concat([config]),
.mergeMap(result => result.config)
.filter(Boolean)
.last(),
// all enabled PluginSpec objects
spec$: extendConfig$

View file

@ -77,21 +77,21 @@ describe('plugin discovery/extend config service', () => {
});
it('adds the schema for a plugin spec to its config prefix', async () => {
const config = Config.withDefaultSchema();
const config = await Config.withDefaultSchema();
expect(config.has('foo.bar.baz')).to.be(false);
await extendConfigService(pluginSpec, config);
expect(config.has('foo.bar.baz')).to.be(true);
});
it('initializes it with the default settings', async () => {
const config = Config.withDefaultSchema();
const config = await Config.withDefaultSchema();
await extendConfigService(pluginSpec, config);
expect(config.get('foo.bar.baz.enabled')).to.be(true);
expect(config.get('foo.bar.baz.test')).to.be('bonk');
});
it('initializes it with values from root settings if defined', async () => {
const config = Config.withDefaultSchema();
const config = await Config.withDefaultSchema();
await extendConfigService(pluginSpec, config, {
foo: {
bar: {
@ -106,7 +106,7 @@ describe('plugin discovery/extend config service', () => {
});
it('throws if root settings are invalid', async () => {
const config = Config.withDefaultSchema();
const config = await Config.withDefaultSchema();
try {
await extendConfigService(pluginSpec, config, {
foo: {
@ -126,7 +126,7 @@ describe('plugin discovery/extend config service', () => {
});
it('calls logDeprecation() with deprecation messages', async () => {
const config = Config.withDefaultSchema();
const config = await Config.withDefaultSchema();
const logDeprecation = sinon.stub();
await extendConfigService(pluginSpec, config, {
foo: {
@ -142,7 +142,7 @@ describe('plugin discovery/extend config service', () => {
});
it('uses settings after transforming deprecations', async () => {
const config = Config.withDefaultSchema();
const config = await Config.withDefaultSchema();
await extendConfigService(pluginSpec, config, {
foo: {
bar: {
@ -158,7 +158,7 @@ describe('plugin discovery/extend config service', () => {
describe('disableConfigExtension()', () => {
it('removes added config', async () => {
const config = Config.withDefaultSchema();
const config = await Config.withDefaultSchema();
await extendConfigService(pluginSpec, config);
expect(config.has('foo.bar.baz.test')).to.be(true);
await disableConfigExtension(pluginSpec, config);
@ -166,7 +166,7 @@ describe('plugin discovery/extend config service', () => {
});
it('leaves {configPrefix}.enabled config', async () => {
const config = Config.withDefaultSchema();
const config = await Config.withDefaultSchema();
expect(config.has('foo.bar.baz.enabled')).to.be(false);
await extendConfigService(pluginSpec, config);
expect(config.get('foo.bar.baz.enabled')).to.be(true);

View file

@ -0,0 +1,62 @@
import { resolve } from 'path';
import { Observable } from 'rxjs';
import expect from 'expect.js';
import { createPack$ } from '../create_pack';
import { PluginPack } from '../plugin_pack';
import {
PLUGINS_DIR,
assertInvalidPackError,
} from './utils';
describe('plugin discovery/create pack', () => {
it('creates PluginPack', async () => {
const packageJson$ = Observable.from([
{
packageJson: {
directoryPath: resolve(PLUGINS_DIR, 'prebuilt'),
contents: {
name: 'prebuilt'
}
}
}
]);
const results = await createPack$(packageJson$).toArray().toPromise();
expect(results).to.have.length(1);
expect(results[0]).to.only.have.keys(['pack']);
const { pack } = results[0];
expect(pack).to.be.a(PluginPack);
});
describe('errors thrown', () => {
async function checkError(path, check) {
const packageJson$ = Observable.from([{
packageJson: {
directoryPath: path
}
}]);
const results = await createPack$(packageJson$).toArray().toPromise();
expect(results).to.have.length(1);
expect(results[0]).to.only.have.keys(['error']);
const { error } = results[0];
await check(error);
}
it('default export is an object', () => checkError(resolve(PLUGINS_DIR, 'exports_object'), error => {
assertInvalidPackError(error);
expect(error.message).to.contain('must export a function');
}));
it('default export is an number', () => checkError(resolve(PLUGINS_DIR, 'exports_number'), error => {
assertInvalidPackError(error);
expect(error.message).to.contain('must export a function');
}));
it('default export is an string', () => checkError(resolve(PLUGINS_DIR, 'exports_string'), error => {
assertInvalidPackError(error);
expect(error.message).to.contain('must export a function');
}));
it('directory with code that fails when required', () => checkError(resolve(PLUGINS_DIR, 'broken_code'), error => {
expect(error.message).to.contain('Cannot find module \'does-not-exist\'');
}));
});
});

View file

@ -2,8 +2,7 @@ import { resolve } from 'path';
import expect from 'expect.js';
import { createPackAtPath$ } from '../pack_at_path';
import { PluginPack } from '../plugin_pack';
import { createPackageJsonAtPath$ } from '../package_json_at_path';
import {
PLUGINS_DIR,
assertInvalidPackError,
@ -12,20 +11,22 @@ import {
describe('plugin discovery/plugin_pack', () => {
describe('createPackAtPath$()', () => {
describe('createPackageJsonAtPath$()', () => {
it('returns an observable', () => {
expect(createPackAtPath$())
expect(createPackageJsonAtPath$())
.to.have.property('subscribe').a('function');
});
it('gets the default provider from prebuilt babel modules', async () => {
const results = await createPackAtPath$(resolve(PLUGINS_DIR, 'prebuilt')).toArray().toPromise();
const results = await createPackageJsonAtPath$(resolve(PLUGINS_DIR, 'prebuilt')).toArray().toPromise();
expect(results).to.have.length(1);
expect(results[0]).to.only.have.keys(['pack']);
expect(results[0].pack).to.be.a(PluginPack);
expect(results[0]).to.only.have.keys(['packageJson']);
expect(results[0].packageJson).to.be.an(Object);
expect(results[0].packageJson.directoryPath).to.be(resolve(PLUGINS_DIR, 'prebuilt'));
expect(results[0].packageJson.contents).to.eql({ name: 'prebuilt' });
});
describe('errors emitted as { error } results', () => {
async function checkError(path, check) {
const results = await createPackAtPath$(path).toArray().toPromise();
const results = await createPackageJsonAtPath$(path).toArray().toPromise();
expect(results).to.have.length(1);
expect(results[0]).to.only.have.keys(['error']);
const { error } = results[0];
@ -59,21 +60,6 @@ describe('plugin discovery/plugin_pack', () => {
assertInvalidPackError(error);
expect(error.message).to.contain('must have a valid package.json file');
}));
it('default export is an object', () => checkError(resolve(PLUGINS_DIR, 'exports_object'), error => {
assertInvalidPackError(error);
expect(error.message).to.contain('must export a function');
}));
it('default export is an number', () => checkError(resolve(PLUGINS_DIR, 'exports_number'), error => {
assertInvalidPackError(error);
expect(error.message).to.contain('must export a function');
}));
it('default export is an string', () => checkError(resolve(PLUGINS_DIR, 'exports_string'), error => {
assertInvalidPackError(error);
expect(error.message).to.contain('must export a function');
}));
it('directory with code that fails when required', () => checkError(resolve(PLUGINS_DIR, 'broken_code'), error => {
expect(error.message).to.contain('Cannot find module \'does-not-exist\'');
}));
});
});
});

View file

@ -2,8 +2,7 @@ import { resolve } from 'path';
import expect from 'expect.js';
import { createPacksInDirectory$ } from '../packs_in_directory';
import { PluginPack } from '../plugin_pack';
import { createPackageJsonsInDirectory$ } from '../package_jsons_in_directory';
import {
PLUGINS_DIR,
@ -11,10 +10,10 @@ import {
} from './utils';
describe('plugin discovery/packs in directory', () => {
describe('createPacksInDirectory$()', () => {
describe('createPackageJsonsInDirectory$()', () => {
describe('errors emitted as { error } results', () => {
async function checkError(path, check) {
const results = await createPacksInDirectory$(path).toArray().toPromise();
const results = await createPackageJsonsInDirectory$(path).toArray().toPromise();
expect(results).to.have.length(1);
expect(results[0]).to.only.have.keys('error');
const { error } = results[0];
@ -43,24 +42,24 @@ describe('plugin discovery/packs in directory', () => {
}));
});
it('includes child errors for invalid packs within a valid directory', async () => {
const results = await createPacksInDirectory$(PLUGINS_DIR).toArray().toPromise();
it('includes child errors for invalid packageJsons within a valid directory', async () => {
const results = await createPackageJsonsInDirectory$(PLUGINS_DIR).toArray().toPromise();
const errors = results
.map(result => result.error)
.filter(Boolean);
const packs = results
.map(result => result.pack)
const packageJsons = results
.map(result => result.packageJson)
.filter(Boolean);
packs.forEach(pack => expect(pack).to.be.a(PluginPack));
packageJsons.forEach(pack => expect(pack).to.be.an(Object));
// there should be one result for each item in PLUGINS_DIR
expect(results).to.have.length(9);
// six of the fixtures are errors of some sort
expect(errors).to.have.length(7);
// two of them are valid
expect(packs).to.have.length(2);
// three of the fixtures are errors of some sort
expect(errors).to.have.length(3);
// six of them are valid
expect(packageJsons).to.have.length(6);
});
});
});

View file

@ -0,0 +1,34 @@
import { PluginPack } from './plugin_pack';
import { createInvalidPackError } from '../errors';
function createPack(packageJson) {
let provider = require(packageJson.directoryPath);
if (provider.__esModule) {
provider = provider.default;
}
if (typeof provider !== 'function') {
throw createInvalidPackError(packageJson.directoryPath, 'must export a function');
}
return new PluginPack({ path: packageJson.directoryPath, pkg: packageJson.contents, provider });
}
export const createPack$ = (packageJson$) => (
packageJson$
.map(({ error, packageJson }) => {
if (error) {
return { error };
}
if (!packageJson) {
throw new Error('packageJson is required to create the pack');
}
return {
pack: createPack(packageJson)
};
})
// createPack can throw errors, and we want them to be represented
// like the errors we consume from createPackageJsonAtPath/Directory
.catch(error => [{ error }])
);

View file

@ -1,3 +1,4 @@
export { createPack$ } from './create_pack';
export { createPackageJsonAtPath$ } from './package_json_at_path';
export { createPackageJsonsInDirectory$ } from './package_jsons_in_directory';
export { PluginPack } from './plugin_pack';
export { createPackAtPath$ } from './pack_at_path';
export { createPacksInDirectory$ } from './packs_in_directory';

View file

@ -1,41 +0,0 @@
import { Observable } from 'rxjs';
import { resolve } from 'path';
import { createInvalidPackError } from '../errors';
import { isDirectory } from './lib';
import { PluginPack } from './plugin_pack';
async function createPackAtPath(path) {
if (!await isDirectory(path)) {
throw createInvalidPackError(path, 'must be a directory');
}
let pkg;
try {
pkg = require(resolve(path, 'package.json'));
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
throw createInvalidPackError(path, 'must have a package.json file');
}
}
if (!pkg || typeof pkg !== 'object') {
throw createInvalidPackError(path, 'must have a valid package.json file');
}
let provider = require(path);
if (provider.__esModule) {
provider = provider.default;
}
if (typeof provider !== 'function') {
throw createInvalidPackError(path, 'must export a function');
}
return new PluginPack({ path, pkg, provider });
}
export const createPackAtPath$ = (path) => (
Observable.defer(() => createPackAtPath(path))
.map(pack => ({ pack }))
.catch(error => [{ error }])
);

View file

@ -0,0 +1,37 @@
import { readFileSync } from 'fs';
import { Observable } from 'rxjs';
import { resolve } from 'path';
import { createInvalidPackError } from '../errors';
import { isDirectory } from './lib';
async function createPackageJsonAtPath(path) {
if (!await isDirectory(path)) {
throw createInvalidPackError(path, 'must be a directory');
}
let str;
try {
str = readFileSync(resolve(path, 'package.json'));
} catch (err) {
throw createInvalidPackError(path, 'must have a package.json file');
}
let pkg;
try {
pkg = JSON.parse(str);
} catch (err) {
throw createInvalidPackError(path, 'must have a valid package.json file');
}
return {
directoryPath: path,
contents: pkg,
};
}
export const createPackageJsonAtPath$ = (path) => (
Observable.defer(() => createPackageJsonAtPath(path))
.map(packageJson => ({ packageJson }))
.catch(error => [{ error }])
);

View file

@ -1,7 +1,7 @@
import { isInvalidDirectoryError } from '../errors';
import { createChildDirectory$ } from './lib';
import { createPackAtPath$ } from './pack_at_path';
import { createPackageJsonAtPath$ } from './package_json_at_path';
/**
* Finds the plugins within a directory. Results are
@ -16,9 +16,9 @@ import { createPackAtPath$ } from './pack_at_path';
* @param {String} path
* @return {Array<{pack}|{error}>}
*/
export const createPacksInDirectory$ = (path) => (
export const createPackageJsonsInDirectory$ = (path) => (
createChildDirectory$(path)
.mergeMap(createPackAtPath$)
.mergeMap(createPackageJsonAtPath$)
.catch(error => {
// this error is produced by createChildDirectory$() when the path
// is invalid, we return them as an error result similar to how

View file

@ -9,8 +9,9 @@ const schemaExts = Symbol('Schema Extensions');
const vals = Symbol('config values');
export class Config {
static withDefaultSchema(settings = {}) {
return new Config(createDefaultSchema(), settings);
static async withDefaultSchema(settings = {}) {
const defaultSchema = await createDefaultSchema();
return new Config(defaultSchema, settings);
}
constructor(initialSchema, initialSettings) {

View file

@ -5,7 +5,7 @@ import os from 'os';
import { fromRoot } from '../../utils';
import { getData } from '../path';
export default () => Joi.object({
export default async () => Joi.object({
pkg: Joi.object({
version: Joi.string().default(Joi.ref('$version')),
branch: Joi.string().default(Joi.ref('$branch')),

View file

@ -4,7 +4,7 @@ import { set } from 'lodash';
describe('Config schema', function () {
let schema;
beforeEach(() => schema = schemaProvider());
beforeEach(async () => schema = await schemaProvider());
function validate(data, options) {
return Joi.validate(data, schema, options);

View file

@ -1,7 +1,7 @@
import { Config } from './config';
import { transformDeprecations } from './transform_deprecations';
export default function (kbnServer) {
export default async function (kbnServer) {
const settings = transformDeprecations(kbnServer.settings);
kbnServer.config = Config.withDefaultSchema(settings);
kbnServer.config = await Config.withDefaultSchema(settings);
}

View file

@ -140,8 +140,8 @@ export default class KbnServer {
return await this.server.inject(opts);
}
applyLoggingConfiguration(settings) {
const config = Config.withDefaultSchema(settings);
async applyLoggingConfiguration(settings) {
const config = await Config.withDefaultSchema(settings);
const loggingOptions = loggingConfiguration(config);
const subset = {
ops: config.get('ops'),

View file

@ -15,12 +15,12 @@ import { uiSettingsMixin } from '../ui_settings_mixin';
describe('uiSettingsMixin()', () => {
const sandbox = sinon.sandbox.create();
function setup(options = {}) {
async function setup(options = {}) {
const {
enabled = true
} = options;
const config = Config.withDefaultSchema({
const config = await Config.withDefaultSchema({
uiSettings: { enabled }
});
@ -70,8 +70,8 @@ describe('uiSettingsMixin()', () => {
afterEach(() => sandbox.restore());
describe('server.uiSettingsServiceFactory()', () => {
it('decorates server with "uiSettingsServiceFactory"', () => {
const { decorations } = setup();
it('decorates server with "uiSettingsServiceFactory"', async () => {
const { decorations } = await setup();
expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function');
sandbox.stub(uiSettingsServiceFactoryNS, 'uiSettingsServiceFactory');
@ -80,8 +80,8 @@ describe('uiSettingsMixin()', () => {
sinon.assert.calledOnce(uiSettingsServiceFactory);
});
it('passes `server` and `options` argument to factory', () => {
const { decorations, server } = setup();
it('passes `server` and `options` argument to factory', async () => {
const { decorations, server } = await setup();
expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function');
sandbox.stub(uiSettingsServiceFactoryNS, 'uiSettingsServiceFactory');
@ -98,8 +98,8 @@ describe('uiSettingsMixin()', () => {
});
describe('request.getUiSettingsService()', () => {
it('exposes "getUiSettingsService" on requests', () => {
const { decorations } = setup();
it('exposes "getUiSettingsService" on requests', async () => {
const { decorations } = await setup();
expect(decorations.request).to.have.property('getUiSettingsService').a('function');
sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest');
@ -108,8 +108,8 @@ describe('uiSettingsMixin()', () => {
sinon.assert.calledOnce(getUiSettingsServiceForRequest);
});
it('passes request to getUiSettingsServiceForRequest', () => {
const { server, decorations } = setup();
it('passes request to getUiSettingsServiceForRequest', async () => {
const { server, decorations } = await setup();
expect(decorations.request).to.have.property('getUiSettingsService').a('function');
sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest');
@ -121,8 +121,8 @@ describe('uiSettingsMixin()', () => {
});
describe('server.uiSettings()', () => {
it('throws an error, links to pr', () => {
const { decorations } = setup();
it('throws an error, links to pr', async () => {
const { decorations } = await setup();
expect(decorations.server).to.have.property('uiSettings').a('function');
expect(() => {
decorations.server.uiSettings();