Add ContextService to server (#42395)
This commit is contained in:
parent
98b445a6e0
commit
5192dac0b6
|
@ -95,8 +95,10 @@ export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) =
|
|||
class VizRenderingPlugin {
|
||||
private readonly vizRenderers = new Map<string, ((domElement: HTMLElement) => () => void)>();
|
||||
|
||||
constructor(private readonly initContext: PluginInitializerContext) {}
|
||||
|
||||
setup(core) {
|
||||
this.contextContainer = core.createContextContainer<
|
||||
this.contextContainer = core.context.createContextContainer<
|
||||
VizRenderContext,
|
||||
ReturnType<VizRenderer>,
|
||||
[HTMLElement]
|
||||
|
@ -110,8 +112,8 @@ class VizRenderingPlugin {
|
|||
}
|
||||
|
||||
start(core) {
|
||||
// Register the core context available to all renderers. Use the VizRendererContext's pluginId as the first arg.
|
||||
this.contextContainer.registerContext('viz_rendering', 'core', () => ({
|
||||
// Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg.
|
||||
this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({
|
||||
i18n: core.i18n,
|
||||
uiSettings: core.uiSettings
|
||||
}));
|
||||
|
|
|
@ -74,6 +74,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [IContextHandler](./kibana-plugin-public.icontexthandler.md) | A function registered by a plugin to perform some action. |
|
||||
| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
|
||||
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
|
||||
| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | |
|
||||
| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | |
|
||||
| [ToastInput](./kibana-plugin-public.toastinput.md) | |
|
||||
| [UiSettingsClientContract](./kibana-plugin-public.uisettingsclientcontract.md) | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) |
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md)
|
||||
|
||||
## PluginOpaqueId type
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type PluginOpaqueId = symbol;
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ContextSetup](./kibana-plugin-server.contextsetup.md) > [createContextContainer](./kibana-plugin-server.contextsetup.createcontextcontainer.md)
|
||||
|
||||
## ContextSetup.createContextContainer() method
|
||||
|
||||
Creates a new for a service owner.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
createContextContainer<TContext extends {}, THandlerReturn, THandlerParmaters extends any[] = []>(): IContextContainer<TContext, THandlerReturn, THandlerParmaters>;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
`IContextContainer<TContext, THandlerReturn, THandlerParmaters>`
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ContextSetup](./kibana-plugin-server.contextsetup.md)
|
||||
|
||||
## ContextSetup interface
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface ContextSetup
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [createContextContainer()](./kibana-plugin-server.contextsetup.createcontextcontainer.md) | Creates a new for a service owner. |
|
||||
|
||||
## Example
|
||||
|
||||
Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts.
|
||||
|
||||
```ts
|
||||
export interface VizRenderContext {
|
||||
core: {
|
||||
i18n: I18nStart;
|
||||
uiSettings: UISettingsClientContract;
|
||||
}
|
||||
[contextName: string]: unknown;
|
||||
}
|
||||
|
||||
export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void;
|
||||
|
||||
class VizRenderingPlugin {
|
||||
private readonly vizRenderers = new Map<string, ((domElement: HTMLElement) => () => void)>();
|
||||
|
||||
constructor(private readonly initContext: PluginInitializerContext) {}
|
||||
|
||||
setup(core) {
|
||||
this.contextContainer = core.context.createContextContainer<
|
||||
VizRenderContext,
|
||||
ReturnType<VizRenderer>,
|
||||
[HTMLElement]
|
||||
>();
|
||||
|
||||
return {
|
||||
registerContext: this.contextContainer.registerContext,
|
||||
registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) =>
|
||||
this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)),
|
||||
};
|
||||
}
|
||||
|
||||
start(core) {
|
||||
// Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg.
|
||||
this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({
|
||||
i18n: core.i18n,
|
||||
uiSettings: core.uiSettings
|
||||
}));
|
||||
|
||||
return {
|
||||
registerContext: this.contextContainer.registerContext,
|
||||
|
||||
renderVizualization: (renderMethod: string, domElement: HTMLElement) => {
|
||||
if (!this.vizRenderer.has(renderMethod)) {
|
||||
throw new Error(`Render method '${renderMethod}' has not been registered`);
|
||||
}
|
||||
|
||||
// The handler can now be called directly with only an `HTMLElement` and will automatically
|
||||
// have a new `context` object created and populated by the context container.
|
||||
const handler = this.vizRenderers.get(renderMethod)
|
||||
return handler(domElement);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [context](./kibana-plugin-server.coresetup.context.md)
|
||||
|
||||
## CoreSetup.context property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
context: {
|
||||
createContextContainer: ContextSetup['createContextContainer'];
|
||||
};
|
||||
```
|
|
@ -16,6 +16,7 @@ export interface CoreSetup
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [context](./kibana-plugin-server.coresetup.context.md) | <code>{</code><br/><code> createContextContainer: ContextSetup['createContextContainer'];</code><br/><code> }</code> | |
|
||||
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>{</code><br/><code> adminClient$: Observable<ClusterClient>;</code><br/><code> dataClient$: Observable<ClusterClient>;</code><br/><code> createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ClusterClient;</code><br/><code> }</code> | |
|
||||
| [http](./kibana-plugin-server.coresetup.http.md) | <code>{</code><br/><code> createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];</code><br/><code> registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];</code><br/><code> registerAuth: HttpServiceSetup['registerAuth'];</code><br/><code> registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];</code><br/><code> basePath: HttpServiceSetup['basePath'];</code><br/><code> isTlsEnabled: HttpServiceSetup['isTlsEnabled'];</code><br/><code> }</code> | |
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of an incoming request authentication. |
|
||||
| [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. |
|
||||
| [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. |
|
||||
| [ContextSetup](./kibana-plugin-server.contextsetup.md) | |
|
||||
| [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins <code>setup</code> method. |
|
||||
| [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins <code>start</code> method. |
|
||||
| [CustomHttpResponseOptions](./kibana-plugin-server.customhttpresponseoptions.md) | HTTP response parameters for a response with adjustable status code. |
|
||||
|
@ -118,6 +119,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | |
|
||||
| [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>server</code> directory should conform to this interface. |
|
||||
| [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. |
|
||||
| [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md) | |
|
||||
| [RecursiveReadonly](./kibana-plugin-server.recursivereadonly.md) | |
|
||||
| [RedirectResponseOptions](./kibana-plugin-server.redirectresponseoptions.md) | HTTP response parameters for redirection response |
|
||||
| [RequestHandler](./kibana-plugin-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) functions. |
|
||||
|
|
|
@ -19,4 +19,5 @@ export interface PluginInitializerContext<ConfigSchema = unknown>
|
|||
| [config](./kibana-plugin-server.plugininitializercontext.config.md) | <code>{</code><br/><code> create: <T = ConfigSchema>() => Observable<T>;</code><br/><code> createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;</code><br/><code> }</code> | |
|
||||
| [env](./kibana-plugin-server.plugininitializercontext.env.md) | <code>{</code><br/><code> mode: EnvironmentMode;</code><br/><code> }</code> | |
|
||||
| [logger](./kibana-plugin-server.plugininitializercontext.logger.md) | <code>LoggerFactory</code> | |
|
||||
| [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md) | <code>PluginOpaqueId</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) > [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md)
|
||||
|
||||
## PluginInitializerContext.opaqueId property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
opaqueId: PluginOpaqueId;
|
||||
```
|
|
@ -0,0 +1,12 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md)
|
||||
|
||||
## PluginOpaqueId type
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type PluginOpaqueId = symbol;
|
||||
```
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { ContextService, ContextSetup } from './context_service';
|
||||
import { contextMock } from './context.mock';
|
||||
import { contextMock } from '../../utils/context.mock';
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<ContextSetup> = {
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { contextMock } from './context.mock';
|
||||
import { contextMock } from '../../utils/context.mock';
|
||||
|
||||
export const MockContextConstructor = jest.fn(contextMock.create);
|
||||
jest.doMock('./context', () => ({
|
||||
jest.doMock('../../utils/context', () => ({
|
||||
ContextContainer: MockContextConstructor,
|
||||
}));
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { PluginOpaqueId } from '../../server';
|
||||
import { MockContextConstructor } from './context_service.test.mocks';
|
||||
import { ContextService } from './context_service';
|
||||
import { PluginOpaqueId } from '../plugins';
|
||||
|
||||
const pluginDependencies = new Map<PluginOpaqueId, PluginOpaqueId[]>();
|
||||
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { IContextContainer, ContextContainer } from './context';
|
||||
import { PluginOpaqueId } from '../../server';
|
||||
import { IContextContainer, ContextContainer } from '../../utils/context';
|
||||
import { CoreContext } from '../core_system';
|
||||
import { PluginOpaqueId } from '../plugins';
|
||||
|
||||
interface StartDeps {
|
||||
pluginDependencies: ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]>;
|
||||
|
@ -64,8 +64,10 @@ export class ContextService {
|
|||
* class VizRenderingPlugin {
|
||||
* private readonly vizRenderers = new Map<string, ((domElement: HTMLElement) => () => void)>();
|
||||
*
|
||||
* constructor(private readonly initContext: PluginInitializerContext) {}
|
||||
*
|
||||
* setup(core) {
|
||||
* this.contextContainer = core.createContextContainer<
|
||||
* this.contextContainer = core.context.createContextContainer<
|
||||
* VizRenderContext,
|
||||
* ReturnType<VizRenderer>,
|
||||
* [HTMLElement]
|
||||
|
@ -79,8 +81,8 @@ export class ContextService {
|
|||
* }
|
||||
*
|
||||
* start(core) {
|
||||
* // Register the core context available to all renderers. Use the VizRendererContext's pluginId as the first arg.
|
||||
* this.contextContainer.registerContext('viz_rendering', 'core', () => ({
|
||||
* // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg.
|
||||
* this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({
|
||||
* i18n: core.i18n,
|
||||
* uiSettings: core.uiSettings
|
||||
* }));
|
||||
|
|
|
@ -18,4 +18,4 @@
|
|||
*/
|
||||
|
||||
export { ContextService, ContextSetup } from './context_service';
|
||||
export { IContextContainer, IContextProvider, IContextHandler } from './context';
|
||||
export { IContextContainer, IContextProvider, IContextHandler } from '../../utils/context';
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import './core.css';
|
||||
|
||||
import { CoreId } from '../server';
|
||||
import { InternalCoreSetup, InternalCoreStart } from '.';
|
||||
import { ChromeService } from './chrome';
|
||||
import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors';
|
||||
|
@ -44,9 +45,6 @@ interface Params {
|
|||
useLegacyTestHarness?: LegacyPlatformParams['useLegacyTestHarness'];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type CoreId = symbol;
|
||||
|
||||
/** @internal */
|
||||
export interface CoreContext {
|
||||
coreId: CoreId;
|
||||
|
|
|
@ -62,7 +62,7 @@ import {
|
|||
ToastsApi,
|
||||
} from './notifications';
|
||||
import { OverlayRef, OverlayStart } from './overlays';
|
||||
import { Plugin, PluginInitializer, PluginInitializerContext } from './plugins';
|
||||
import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins';
|
||||
import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './ui_settings';
|
||||
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
|
||||
import { DocLinksStart } from './doc_links';
|
||||
|
@ -181,6 +181,7 @@ export {
|
|||
Plugin,
|
||||
PluginInitializer,
|
||||
PluginInitializerContext,
|
||||
PluginOpaqueId,
|
||||
Toast,
|
||||
ToastInput,
|
||||
ToastsApi,
|
||||
|
|
|
@ -18,5 +18,6 @@
|
|||
*/
|
||||
|
||||
export * from './plugins_service';
|
||||
export { Plugin, PluginInitializer, PluginOpaqueId } from './plugin';
|
||||
export { Plugin, PluginInitializer } from './plugin';
|
||||
export { PluginInitializerContext } from './plugin_context';
|
||||
export { PluginOpaqueId } from '../../server/types';
|
||||
|
|
|
@ -17,14 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { DiscoveredPlugin } from '../../server';
|
||||
import { DiscoveredPlugin, PluginOpaqueId } from '../../server';
|
||||
import { PluginInitializerContext } from './plugin_context';
|
||||
import { loadPluginBundle } from './plugin_loader';
|
||||
import { CoreStart, CoreSetup } from '..';
|
||||
|
||||
/** @public */
|
||||
export type PluginOpaqueId = symbol;
|
||||
|
||||
/**
|
||||
* The interface that should be returned by a `PluginInitializer`.
|
||||
*
|
||||
|
|
|
@ -19,9 +19,9 @@
|
|||
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import { DiscoveredPlugin } from '../../server';
|
||||
import { DiscoveredPlugin, PluginOpaqueId } from '../../server';
|
||||
import { CoreContext } from '../core_system';
|
||||
import { PluginWrapper, PluginOpaqueId } from './plugin';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
|
||||
import { CoreSetup, CoreStart } from '../';
|
||||
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { DiscoveredPlugin, PluginName } from '../../server';
|
||||
import { DiscoveredPlugin, PluginName, PluginOpaqueId } from '../../server';
|
||||
import { CoreService } from '../../types';
|
||||
import { CoreContext } from '../core_system';
|
||||
import { PluginWrapper, PluginOpaqueId } from './plugin';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import {
|
||||
createPluginInitializerContext,
|
||||
createPluginSetupContext,
|
||||
|
|
|
@ -492,7 +492,6 @@ export interface I18nStart {
|
|||
export interface IContextContainer<TContext extends {}, THandlerReturn, THandlerParameters extends any[] = []> {
|
||||
// Warning: (ae-forgotten-export) The symbol "Promisify" needs to be exported by the entry point index.d.ts
|
||||
createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler<TContext, THandlerReturn, THandlerParameters>): (...rest: THandlerParameters) => Promisify<THandlerReturn>;
|
||||
// Warning: (ae-forgotten-export) The symbol "PluginOpaqueId" needs to be exported by the entry point index.d.ts
|
||||
registerContext<TContextName extends keyof TContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider<TContext, TContextName, THandlerParameters>): this;
|
||||
}
|
||||
|
||||
|
@ -595,6 +594,9 @@ export interface PluginInitializerContext {
|
|||
readonly opaqueId: PluginOpaqueId;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type PluginOpaqueId = symbol;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
|
42
src/core/server/context/context_service.mock.ts
Normal file
42
src/core/server/context/context_service.mock.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { ContextService, ContextSetup } from './context_service';
|
||||
import { contextMock } from '../../utils/context.mock';
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<ContextSetup> = {
|
||||
createContextContainer: jest.fn().mockImplementation(() => contextMock.create()),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
type ContextServiceContract = PublicMethodsOf<ContextService>;
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<ContextServiceContract> = {
|
||||
setup: jest.fn(),
|
||||
};
|
||||
mocked.setup.mockReturnValue(createSetupContractMock());
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const contextServiceMock = {
|
||||
create: createMock,
|
||||
createSetupContract: createSetupContractMock,
|
||||
};
|
25
src/core/server/context/context_service.test.mocks.ts
Normal file
25
src/core/server/context/context_service.test.mocks.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { contextMock } from '../../utils/context.mock';
|
||||
|
||||
export const MockContextConstructor = jest.fn(contextMock.create);
|
||||
jest.doMock('../../utils/context', () => ({
|
||||
ContextContainer: MockContextConstructor,
|
||||
}));
|
37
src/core/server/context/context_service.test.ts
Normal file
37
src/core/server/context/context_service.test.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { PluginOpaqueId } from '../../server';
|
||||
import { MockContextConstructor } from './context_service.test.mocks';
|
||||
import { ContextService } from './context_service';
|
||||
import { CoreContext } from '../core_context';
|
||||
|
||||
const pluginDependencies = new Map<PluginOpaqueId, PluginOpaqueId[]>();
|
||||
|
||||
describe('ContextService', () => {
|
||||
describe('#setup()', () => {
|
||||
test('createContextContainer returns a new container configured with pluginDependencies', () => {
|
||||
const coreId = Symbol();
|
||||
const service = new ContextService({ coreId } as CoreContext);
|
||||
const setup = service.setup({ pluginDependencies });
|
||||
expect(setup.createContextContainer()).toBeDefined();
|
||||
expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, coreId);
|
||||
});
|
||||
});
|
||||
});
|
120
src/core/server/context/context_service.ts
Normal file
120
src/core/server/context/context_service.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { PluginOpaqueId } from '../../server';
|
||||
import { IContextContainer, ContextContainer } from '../../utils/context';
|
||||
import { CoreContext } from '../core_context';
|
||||
|
||||
interface SetupDeps {
|
||||
pluginDependencies: ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ContextService {
|
||||
constructor(private readonly core: CoreContext) {}
|
||||
|
||||
public setup({ pluginDependencies }: SetupDeps): ContextSetup {
|
||||
return {
|
||||
createContextContainer: <
|
||||
TContext extends {},
|
||||
THandlerReturn,
|
||||
THandlerParameters extends any[] = []
|
||||
>() => {
|
||||
return new ContextContainer<TContext, THandlerReturn, THandlerParameters>(
|
||||
pluginDependencies,
|
||||
this.core.coreId
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc IContextContainer}
|
||||
*
|
||||
* @example
|
||||
* Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we
|
||||
* want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts.
|
||||
* ```ts
|
||||
* export interface VizRenderContext {
|
||||
* core: {
|
||||
* i18n: I18nStart;
|
||||
* uiSettings: UISettingsClientContract;
|
||||
* }
|
||||
* [contextName: string]: unknown;
|
||||
* }
|
||||
*
|
||||
* export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void;
|
||||
*
|
||||
* class VizRenderingPlugin {
|
||||
* private readonly vizRenderers = new Map<string, ((domElement: HTMLElement) => () => void)>();
|
||||
*
|
||||
* constructor(private readonly initContext: PluginInitializerContext) {}
|
||||
*
|
||||
* setup(core) {
|
||||
* this.contextContainer = core.context.createContextContainer<
|
||||
* VizRenderContext,
|
||||
* ReturnType<VizRenderer>,
|
||||
* [HTMLElement]
|
||||
* >();
|
||||
*
|
||||
* return {
|
||||
* registerContext: this.contextContainer.registerContext,
|
||||
* registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) =>
|
||||
* this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)),
|
||||
* };
|
||||
* }
|
||||
*
|
||||
* start(core) {
|
||||
* // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg.
|
||||
* this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({
|
||||
* i18n: core.i18n,
|
||||
* uiSettings: core.uiSettings
|
||||
* }));
|
||||
*
|
||||
* return {
|
||||
* registerContext: this.contextContainer.registerContext,
|
||||
*
|
||||
* renderVizualization: (renderMethod: string, domElement: HTMLElement) => {
|
||||
* if (!this.vizRenderer.has(renderMethod)) {
|
||||
* throw new Error(`Render method '${renderMethod}' has not been registered`);
|
||||
* }
|
||||
*
|
||||
* // The handler can now be called directly with only an `HTMLElement` and will automatically
|
||||
* // have a new `context` object created and populated by the context container.
|
||||
* const handler = this.vizRenderers.get(renderMethod)
|
||||
* return handler(domElement);
|
||||
* }
|
||||
* };
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ContextSetup {
|
||||
/**
|
||||
* Creates a new {@link IContextContainer} for a service owner.
|
||||
*/
|
||||
createContextContainer<
|
||||
TContext extends {},
|
||||
THandlerReturn,
|
||||
THandlerParmaters extends any[] = []
|
||||
>(): IContextContainer<TContext, THandlerReturn, THandlerParmaters>;
|
||||
}
|
21
src/core/server/context/index.ts
Normal file
21
src/core/server/context/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { ContextService, ContextSetup } from './context_service';
|
||||
export { IContextContainer, IContextProvider, IContextHandler } from '../../utils/context';
|
|
@ -20,12 +20,16 @@
|
|||
import { ConfigService, Env } from './config';
|
||||
import { LoggerFactory } from './logging';
|
||||
|
||||
/** @internal */
|
||||
export type CoreId = symbol;
|
||||
|
||||
/**
|
||||
* Groups all main Kibana's core modules/systems/services that are consumed in a
|
||||
* variety of places within the core itself.
|
||||
* @internal
|
||||
*/
|
||||
export interface CoreContext {
|
||||
coreId: CoreId;
|
||||
env: Env;
|
||||
configService: ConfigService;
|
||||
logger: LoggerFactory;
|
||||
|
|
|
@ -54,7 +54,7 @@ const logger = loggingServiceMock.create();
|
|||
beforeEach(() => {
|
||||
env = Env.createDefault(getEnvOptions());
|
||||
|
||||
coreContext = { env, logger, configService: configService as any };
|
||||
coreContext = { coreId: Symbol(), env, logger, configService: configService as any };
|
||||
elasticsearchService = new ElasticsearchService(coreContext);
|
||||
});
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import { getEnvOptions } from '../config/__mocks__/env';
|
|||
|
||||
const logger = loggingServiceMock.create();
|
||||
const env = Env.createDefault(getEnvOptions());
|
||||
const coreId = Symbol();
|
||||
|
||||
const createConfigService = (value: Partial<HttpConfigType> = {}) => {
|
||||
const configService = new ConfigService(
|
||||
|
@ -68,7 +69,7 @@ test('creates and sets up http server', async () => {
|
|||
};
|
||||
mockHttpServer.mockImplementation(() => httpServer);
|
||||
|
||||
const service = new HttpService({ configService, env, logger });
|
||||
const service = new HttpService({ coreId, configService, env, logger });
|
||||
|
||||
expect(mockHttpServer.mock.instances.length).toBe(1);
|
||||
|
||||
|
@ -103,6 +104,7 @@ test('spins up notReady server until started if configured with `autoListen:true
|
|||
}));
|
||||
|
||||
const service = new HttpService({
|
||||
coreId,
|
||||
configService,
|
||||
env: new Env('.', getEnvOptions()),
|
||||
logger,
|
||||
|
@ -144,7 +146,7 @@ test('logs error if already set up', async () => {
|
|||
};
|
||||
mockHttpServer.mockImplementation(() => httpServer);
|
||||
|
||||
const service = new HttpService({ configService, env, logger });
|
||||
const service = new HttpService({ coreId, configService, env, logger });
|
||||
|
||||
await service.setup();
|
||||
|
||||
|
@ -162,7 +164,7 @@ test('stops http server', async () => {
|
|||
};
|
||||
mockHttpServer.mockImplementation(() => httpServer);
|
||||
|
||||
const service = new HttpService({ configService, env, logger });
|
||||
const service = new HttpService({ coreId, configService, env, logger });
|
||||
|
||||
await service.setup();
|
||||
await service.start();
|
||||
|
@ -189,7 +191,7 @@ test('stops not ready server if it is running', async () => {
|
|||
};
|
||||
mockHttpServer.mockImplementation(() => httpServer);
|
||||
|
||||
const service = new HttpService({ configService, env, logger });
|
||||
const service = new HttpService({ coreId, configService, env, logger });
|
||||
|
||||
await service.setup();
|
||||
|
||||
|
@ -212,7 +214,7 @@ test('register route handler', async () => {
|
|||
};
|
||||
mockHttpServer.mockImplementation(() => httpServer);
|
||||
|
||||
const service = new HttpService({ configService, env, logger });
|
||||
const service = new HttpService({ coreId, configService, env, logger });
|
||||
|
||||
const router = new Router('/foo');
|
||||
const { registerRouter } = await service.setup();
|
||||
|
@ -232,7 +234,7 @@ test('returns http server contract on setup', async () => {
|
|||
stop: noop,
|
||||
}));
|
||||
|
||||
const service = new HttpService({ configService, env, logger });
|
||||
const service = new HttpService({ coreId, configService, env, logger });
|
||||
const setupHttpServer = await service.setup();
|
||||
expect(setupHttpServer).toEqual(httpServer);
|
||||
});
|
||||
|
@ -248,6 +250,7 @@ test('does not start http server if process is dev cluster master', async () =>
|
|||
mockHttpServer.mockImplementation(() => httpServer);
|
||||
|
||||
const service = new HttpService({
|
||||
coreId,
|
||||
configService,
|
||||
env: new Env('.', getEnvOptions({ isDevClusterMaster: true })),
|
||||
logger,
|
||||
|
@ -272,6 +275,7 @@ test('does not start http server if configured with `autoListen:false`', async (
|
|||
mockHttpServer.mockImplementation(() => httpServer);
|
||||
|
||||
const service = new HttpService({
|
||||
coreId,
|
||||
configService,
|
||||
env: new Env('.', getEnvOptions()),
|
||||
logger,
|
||||
|
|
|
@ -42,10 +42,12 @@ import {
|
|||
ElasticsearchServiceSetup,
|
||||
} from './elasticsearch';
|
||||
import { HttpServiceSetup, HttpServiceStart } from './http';
|
||||
import { PluginsServiceSetup, PluginsServiceStart } from './plugins';
|
||||
import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins';
|
||||
import { ContextSetup } from './context';
|
||||
|
||||
export { bootstrap } from './bootstrap';
|
||||
export { ConfigService } from './config';
|
||||
export { CoreId } from './core_context';
|
||||
export {
|
||||
CallAPIOptions,
|
||||
ClusterClient,
|
||||
|
@ -146,6 +148,9 @@ export { RecursiveReadonly } from '../utils';
|
|||
* @public
|
||||
*/
|
||||
export interface CoreSetup {
|
||||
context: {
|
||||
createContextContainer: ContextSetup['createContextContainer'];
|
||||
};
|
||||
elasticsearch: {
|
||||
adminClient$: Observable<ClusterClient>;
|
||||
dataClient$: Observable<ClusterClient>;
|
||||
|
@ -186,9 +191,11 @@ export interface InternalCoreStart {
|
|||
}
|
||||
|
||||
export {
|
||||
ContextSetup,
|
||||
HttpServiceSetup,
|
||||
HttpServiceStart,
|
||||
ElasticsearchServiceSetup,
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
PluginOpaqueId,
|
||||
};
|
||||
|
|
|
@ -37,6 +37,7 @@ import { PluginsServiceSetup, PluginsServiceStart } from '../plugins/plugins_ser
|
|||
|
||||
const MockKbnServer: jest.Mock<KbnServer> = KbnServer as any;
|
||||
|
||||
let coreId: symbol;
|
||||
let env: Env;
|
||||
let config$: BehaviorSubject<Config>;
|
||||
let setupDeps: {
|
||||
|
@ -60,6 +61,7 @@ const logger = loggingServiceMock.create();
|
|||
let configService: ReturnType<typeof configServiceMock.create>;
|
||||
|
||||
beforeEach(() => {
|
||||
coreId = Symbol();
|
||||
env = Env.createDefault(getEnvOptions());
|
||||
configService = configServiceMock.create();
|
||||
|
||||
|
@ -112,7 +114,12 @@ afterEach(() => {
|
|||
describe('once LegacyService is set up with connection info', () => {
|
||||
test('creates legacy kbnServer and calls `listen`.', async () => {
|
||||
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
|
||||
const legacyService = new LegacyService({ env, logger, configService: configService as any });
|
||||
const legacyService = new LegacyService({
|
||||
coreId,
|
||||
env,
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
|
||||
await legacyService.setup(setupDeps);
|
||||
await legacyService.start(startDeps);
|
||||
|
@ -136,7 +143,12 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => {
|
||||
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false }));
|
||||
|
||||
const legacyService = new LegacyService({ env, logger, configService: configService as any });
|
||||
const legacyService = new LegacyService({
|
||||
coreId,
|
||||
env,
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
await legacyService.setup(setupDeps);
|
||||
await legacyService.start(startDeps);
|
||||
|
||||
|
@ -160,7 +172,12 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
test('creates legacy kbnServer and closes it if `listen` fails.', async () => {
|
||||
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
|
||||
MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed'));
|
||||
const legacyService = new LegacyService({ env, logger, configService: configService as any });
|
||||
const legacyService = new LegacyService({
|
||||
coreId,
|
||||
env,
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
|
||||
await legacyService.setup(setupDeps);
|
||||
await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot();
|
||||
|
@ -172,7 +189,12 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
|
||||
test('throws if fails to retrieve initial config.', async () => {
|
||||
configService.getConfig$.mockReturnValue(throwError(new Error('something failed')));
|
||||
const legacyService = new LegacyService({ env, logger, configService: configService as any });
|
||||
const legacyService = new LegacyService({
|
||||
coreId,
|
||||
env,
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
|
||||
await legacyService.setup(setupDeps);
|
||||
await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot();
|
||||
|
@ -182,7 +204,12 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
});
|
||||
|
||||
test('reconfigures logging configuration if new config is received.', async () => {
|
||||
const legacyService = new LegacyService({ env, logger, configService: configService as any });
|
||||
const legacyService = new LegacyService({
|
||||
coreId,
|
||||
env,
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
await legacyService.setup(setupDeps);
|
||||
await legacyService.start(startDeps);
|
||||
|
||||
|
@ -197,7 +224,12 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
});
|
||||
|
||||
test('logs error if re-configuring fails.', async () => {
|
||||
const legacyService = new LegacyService({ env, logger, configService: configService as any });
|
||||
const legacyService = new LegacyService({
|
||||
coreId,
|
||||
env,
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
await legacyService.setup(setupDeps);
|
||||
await legacyService.start(startDeps);
|
||||
|
||||
|
@ -216,7 +248,12 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
});
|
||||
|
||||
test('logs error if config service fails.', async () => {
|
||||
const legacyService = new LegacyService({ env, logger, configService: configService as any });
|
||||
const legacyService = new LegacyService({
|
||||
coreId,
|
||||
env,
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
await legacyService.setup(setupDeps);
|
||||
await legacyService.start(startDeps);
|
||||
|
||||
|
@ -235,7 +272,7 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
describe('once LegacyService is set up without connection info', () => {
|
||||
let legacyService: LegacyService;
|
||||
beforeEach(async () => {
|
||||
legacyService = new LegacyService({ env, logger, configService: configService as any });
|
||||
legacyService = new LegacyService({ coreId, env, logger, configService: configService as any });
|
||||
|
||||
await legacyService.setup(setupDeps);
|
||||
await legacyService.start(startDeps);
|
||||
|
@ -277,6 +314,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
|
|||
|
||||
test('creates ClusterManager without base path proxy.', async () => {
|
||||
const devClusterLegacyService = new LegacyService({
|
||||
coreId,
|
||||
env: Env.createDefault(
|
||||
getEnvOptions({
|
||||
cliArgs: { silent: true, basePath: false },
|
||||
|
@ -297,6 +335,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
|
|||
|
||||
test('creates ClusterManager with base path proxy.', async () => {
|
||||
const devClusterLegacyService = new LegacyService({
|
||||
coreId,
|
||||
env: Env.createDefault(
|
||||
getEnvOptions({
|
||||
cliArgs: { quiet: true, basePath: true },
|
||||
|
@ -320,7 +359,12 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
|
|||
});
|
||||
|
||||
test('Cannot start without setup phase', async () => {
|
||||
const legacyService = new LegacyService({ env, logger, configService: configService as any });
|
||||
const legacyService = new LegacyService({
|
||||
coreId,
|
||||
env,
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Legacy service is not setup yet."`
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart } from '.';
|
|||
import { loggingServiceMock } from './logging/logging_service.mock';
|
||||
import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
|
||||
import { httpServiceMock } from './http/http_service.mock';
|
||||
import { contextServiceMock } from './context/context_service.mock';
|
||||
|
||||
export { httpServerMock } from './http/http_server.mocks';
|
||||
export { sessionStorageMock } from './http/cookie_session_storage.mocks';
|
||||
|
@ -41,6 +42,7 @@ export function pluginInitializerContextConfigMock<T>(config: T) {
|
|||
|
||||
function pluginInitializerContextMock<T>(config: T) {
|
||||
const mock: PluginInitializerContext<T> = {
|
||||
opaqueId: Symbol(),
|
||||
logger: loggingServiceMock.create(),
|
||||
env: {
|
||||
mode: {
|
||||
|
@ -57,6 +59,7 @@ function pluginInitializerContextMock<T>(config: T) {
|
|||
|
||||
function createCoreSetupMock() {
|
||||
const mock: MockedKeys<CoreSetup> = {
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
http: httpServiceMock.createSetupContract(),
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ import { resolve } from 'path';
|
|||
import { coerce } from 'semver';
|
||||
import { promisify } from 'util';
|
||||
import { isConfigPath, PackageInfo } from '../../config';
|
||||
import { PluginManifest } from '../plugin';
|
||||
import { PluginManifest } from '../types';
|
||||
import { PluginDiscoveryError } from './plugin_discovery_error';
|
||||
|
||||
const fsReadFileAsync = promisify(readFile);
|
||||
|
|
|
@ -128,6 +128,7 @@ test('properly iterates through plugin search locations', async () => {
|
|||
.pipe(first())
|
||||
.toPromise();
|
||||
const { plugin$, error$ } = discover(new PluginsConfig(rawConfig, env), {
|
||||
coreId: Symbol(),
|
||||
configService,
|
||||
env,
|
||||
logger,
|
||||
|
|
|
@ -115,11 +115,13 @@ function createPlugin$(path: string, log: Logger, coreContext: CoreContext) {
|
|||
return from(parseManifest(path, coreContext.env.packageInfo)).pipe(
|
||||
map(manifest => {
|
||||
log.debug(`Successfully discovered plugin "${manifest.id}" at "${path}"`);
|
||||
return new PluginWrapper(
|
||||
const opaqueId = Symbol(manifest.id);
|
||||
return new PluginWrapper({
|
||||
path,
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
}),
|
||||
catchError(err => [err])
|
||||
);
|
||||
|
|
|
@ -21,12 +21,4 @@ export { PluginsService, PluginsServiceSetup, PluginsServiceStart } from './plug
|
|||
export { config } from './plugins_config';
|
||||
/** @internal */
|
||||
export { isNewPlatformPlugin } from './discovery';
|
||||
/** @internal */
|
||||
export {
|
||||
DiscoveredPlugin,
|
||||
DiscoveredPluginInternal,
|
||||
Plugin,
|
||||
PluginInitializer,
|
||||
PluginName,
|
||||
} from './plugin';
|
||||
export { PluginInitializerContext } from './plugin_context';
|
||||
export * from './types';
|
||||
|
|
|
@ -29,8 +29,10 @@ import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service
|
|||
import { httpServiceMock } from '../http/http_service.mock';
|
||||
import { loggingServiceMock } from '../logging/logging_service.mock';
|
||||
|
||||
import { PluginWrapper, PluginManifest } from './plugin';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { PluginManifest } from './types';
|
||||
import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context';
|
||||
import { contextServiceMock } from '../context/context_service.mock';
|
||||
|
||||
const mockPluginInitializer = jest.fn();
|
||||
const logger = loggingServiceMock.create();
|
||||
|
@ -63,16 +65,19 @@ function createPluginManifest(manifestProps: Partial<PluginManifest> = {}): Plug
|
|||
const configService = configServiceMock.create();
|
||||
configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true }));
|
||||
|
||||
let coreId: symbol;
|
||||
let env: Env;
|
||||
let coreContext: CoreContext;
|
||||
const setupDeps = {
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
http: httpServiceMock.createSetupContract(),
|
||||
};
|
||||
beforeEach(() => {
|
||||
coreId = Symbol('core');
|
||||
env = Env.createDefault(getEnvOptions());
|
||||
|
||||
coreContext = { env, logger, configService: configService as any };
|
||||
coreContext = { coreId, env, logger, configService: configService as any };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -81,11 +86,13 @@ afterEach(() => {
|
|||
|
||||
test('`constructor` correctly initializes plugin instance', () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'some-plugin-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'some-plugin-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
expect(plugin.name).toBe('some-plugin-id');
|
||||
expect(plugin.configPath).toBe('path');
|
||||
|
@ -96,11 +103,13 @@ test('`constructor` correctly initializes plugin instance', () => {
|
|||
|
||||
test('`setup` fails if `plugin` initializer is not exported', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-without-initializer-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-without-initializer-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
await expect(
|
||||
plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {})
|
||||
|
@ -111,11 +120,13 @@ test('`setup` fails if `plugin` initializer is not exported', async () => {
|
|||
|
||||
test('`setup` fails if plugin initializer is not a function', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-wrong-initializer-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-wrong-initializer-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
await expect(
|
||||
plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {})
|
||||
|
@ -126,11 +137,13 @@ test('`setup` fails if plugin initializer is not a function', async () => {
|
|||
|
||||
test('`setup` fails if initializer does not return object', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-initializer-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-initializer-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
mockPluginInitializer.mockReturnValue(null);
|
||||
|
||||
|
@ -143,11 +156,13 @@ test('`setup` fails if initializer does not return object', async () => {
|
|||
|
||||
test('`setup` fails if object returned from initializer does not define `setup` function', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-initializer-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-initializer-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
const mockPluginInstance = { run: jest.fn() };
|
||||
mockPluginInitializer.mockReturnValue(mockPluginInstance);
|
||||
|
@ -161,8 +176,14 @@ test('`setup` fails if object returned from initializer does not define `setup`
|
|||
|
||||
test('`setup` initializes plugin and calls appropriate lifecycle hook', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const initializerContext = createPluginInitializerContext(coreContext, manifest);
|
||||
const plugin = new PluginWrapper('plugin-with-initializer-path', manifest, initializerContext);
|
||||
const opaqueId = Symbol();
|
||||
const initializerContext = createPluginInitializerContext(coreContext, opaqueId, manifest);
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-initializer-path',
|
||||
manifest,
|
||||
opaqueId,
|
||||
initializerContext,
|
||||
});
|
||||
|
||||
const mockPluginInstance = { setup: jest.fn().mockResolvedValue({ contract: 'yes' }) };
|
||||
mockPluginInitializer.mockReturnValue(mockPluginInstance);
|
||||
|
@ -180,11 +201,13 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async ()
|
|||
|
||||
test('`start` fails if setup is not called first', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'some-plugin-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'some-plugin-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Plugin \\"some-plugin-id\\" can't be started since it isn't set up."`
|
||||
|
@ -193,11 +216,13 @@ test('`start` fails if setup is not called first', async () => {
|
|||
|
||||
test('`start` calls plugin.start with context and dependencies', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-initializer-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-initializer-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
const context = { any: 'thing' } as any;
|
||||
const deps = { otherDep: 'value' };
|
||||
|
||||
|
@ -218,11 +243,13 @@ test('`start` calls plugin.start with context and dependencies', async () => {
|
|||
|
||||
test('`stop` fails if plugin is not set up', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-initializer-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-initializer-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() };
|
||||
mockPluginInitializer.mockReturnValue(mockPluginInstance);
|
||||
|
@ -235,11 +262,13 @@ test('`stop` fails if plugin is not set up', async () => {
|
|||
|
||||
test('`stop` does nothing if plugin does not define `stop` function', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-initializer-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-initializer-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
mockPluginInitializer.mockReturnValue({ setup: jest.fn() });
|
||||
await plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {});
|
||||
|
@ -249,11 +278,13 @@ test('`stop` does nothing if plugin does not define `stop` function', async () =
|
|||
|
||||
test('`stop` calls `stop` defined by the plugin instance', async () => {
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-initializer-path',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-initializer-path',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() };
|
||||
mockPluginInitializer.mockReturnValue(mockPluginInstance);
|
||||
|
@ -276,11 +307,13 @@ describe('#getConfigSchema()', () => {
|
|||
{ virtual: true }
|
||||
);
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-schema',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-schema',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
|
||||
expect(plugin.getConfigSchema()).toBe(pluginSchema);
|
||||
});
|
||||
|
@ -288,21 +321,25 @@ describe('#getConfigSchema()', () => {
|
|||
it('returns null if config definition not specified', () => {
|
||||
jest.doMock('plugin-with-no-definition/server', () => ({}), { virtual: true });
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-no-definition',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-no-definition',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
expect(plugin.getConfigSchema()).toBe(null);
|
||||
});
|
||||
|
||||
it('returns null for plugins without a server part', () => {
|
||||
const manifest = createPluginManifest({ server: false });
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-with-no-definition',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-with-no-definition',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
expect(plugin.getConfigSchema()).toBe(null);
|
||||
});
|
||||
|
||||
|
@ -319,11 +356,13 @@ describe('#getConfigSchema()', () => {
|
|||
{ virtual: true }
|
||||
);
|
||||
const manifest = createPluginManifest();
|
||||
const plugin = new PluginWrapper(
|
||||
'plugin-invalid-schema',
|
||||
const opaqueId = Symbol();
|
||||
const plugin = new PluginWrapper({
|
||||
path: 'plugin-invalid-schema',
|
||||
manifest,
|
||||
createPluginInitializerContext(coreContext, manifest)
|
||||
);
|
||||
opaqueId,
|
||||
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
|
||||
});
|
||||
expect(() => plugin.getConfigSchema()).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Configuration schema expected to be an instance of Type"`
|
||||
);
|
||||
|
|
|
@ -22,143 +22,17 @@ import typeDetect from 'type-detect';
|
|||
|
||||
import { Type } from '@kbn/config-schema';
|
||||
|
||||
import { ConfigPath } from '../config';
|
||||
import { Logger } from '../logging';
|
||||
import { PluginInitializerContext } from './plugin_context';
|
||||
import {
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
PluginManifest,
|
||||
PluginConfigSchema,
|
||||
PluginInitializer,
|
||||
PluginOpaqueId,
|
||||
} from './types';
|
||||
import { CoreSetup, CoreStart } from '..';
|
||||
|
||||
export type PluginConfigSchema = Type<unknown> | null;
|
||||
|
||||
/**
|
||||
* Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays
|
||||
* that use it as a key or value more obvious.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PluginName = string;
|
||||
|
||||
/**
|
||||
* Describes the set of required and optional properties plugin can define in its
|
||||
* mandatory JSON manifest file.
|
||||
* @internal
|
||||
*/
|
||||
export interface PluginManifest {
|
||||
/**
|
||||
* Identifier of the plugin.
|
||||
*/
|
||||
readonly id: PluginName;
|
||||
|
||||
/**
|
||||
* Version of the plugin.
|
||||
*/
|
||||
readonly version: string;
|
||||
|
||||
/**
|
||||
* The version of Kibana the plugin is compatible with, defaults to "version".
|
||||
*/
|
||||
readonly kibanaVersion: string;
|
||||
|
||||
/**
|
||||
* Root configuration path used by the plugin, defaults to "id".
|
||||
*/
|
||||
readonly configPath: ConfigPath;
|
||||
|
||||
/**
|
||||
* An optional list of the other plugins that **must be** installed and enabled
|
||||
* for this plugin to function properly.
|
||||
*/
|
||||
readonly requiredPlugins: readonly PluginName[];
|
||||
|
||||
/**
|
||||
* An optional list of the other plugins that if installed and enabled **may be**
|
||||
* leveraged by this plugin for some additional functionality but otherwise are
|
||||
* not required for this plugin to work properly.
|
||||
*/
|
||||
readonly optionalPlugins: readonly PluginName[];
|
||||
|
||||
/**
|
||||
* Specifies whether plugin includes some client/browser specific functionality
|
||||
* that should be included into client bundle via `public/ui_plugin.js` file.
|
||||
*/
|
||||
readonly ui: boolean;
|
||||
|
||||
/**
|
||||
* Specifies whether plugin includes some server-side specific functionality.
|
||||
*/
|
||||
readonly server: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Small container object used to expose information about discovered plugins that may
|
||||
* or may not have been started.
|
||||
* @public
|
||||
*/
|
||||
export interface DiscoveredPlugin {
|
||||
/**
|
||||
* Identifier of the plugin.
|
||||
*/
|
||||
readonly id: PluginName;
|
||||
|
||||
/**
|
||||
* Root configuration path used by the plugin, defaults to "id".
|
||||
*/
|
||||
readonly configPath: ConfigPath;
|
||||
|
||||
/**
|
||||
* An optional list of the other plugins that **must be** installed and enabled
|
||||
* for this plugin to function properly.
|
||||
*/
|
||||
readonly requiredPlugins: readonly PluginName[];
|
||||
|
||||
/**
|
||||
* An optional list of the other plugins that if installed and enabled **may be**
|
||||
* leveraged by this plugin for some additional functionality but otherwise are
|
||||
* not required for this plugin to work properly.
|
||||
*/
|
||||
readonly optionalPlugins: readonly PluginName[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An extended `DiscoveredPlugin` that exposes more sensitive information. Should never
|
||||
* be exposed to client-side code.
|
||||
* @internal
|
||||
*/
|
||||
export interface DiscoveredPluginInternal extends DiscoveredPlugin {
|
||||
/**
|
||||
* Path on the filesystem where plugin was loaded from.
|
||||
*/
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface that should be returned by a `PluginInitializer`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface Plugin<
|
||||
TSetup = void,
|
||||
TStart = void,
|
||||
TPluginsSetup extends object = object,
|
||||
TPluginsStart extends object = object
|
||||
> {
|
||||
setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
|
||||
start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;
|
||||
stop?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `plugin` export at the root of a plugin's `server` directory should conform
|
||||
* to this interface.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PluginInitializer<
|
||||
TSetup,
|
||||
TStart,
|
||||
TPluginsSetup extends object = object,
|
||||
TPluginsStart extends object = object
|
||||
> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
||||
|
||||
/**
|
||||
* Lightweight wrapper around discovered plugin that is responsible for instantiating
|
||||
* plugin and dispatching proper context and dependencies into plugin's lifecycle hooks.
|
||||
|
@ -171,6 +45,9 @@ export class PluginWrapper<
|
|||
TPluginsSetup extends object = object,
|
||||
TPluginsStart extends object = object
|
||||
> {
|
||||
public readonly path: string;
|
||||
public readonly manifest: PluginManifest;
|
||||
public readonly opaqueId: PluginOpaqueId;
|
||||
public readonly name: PluginManifest['id'];
|
||||
public readonly configPath: PluginManifest['configPath'];
|
||||
public readonly requiredPlugins: PluginManifest['requiredPlugins'];
|
||||
|
@ -179,21 +56,29 @@ export class PluginWrapper<
|
|||
public readonly includesUiPlugin: PluginManifest['ui'];
|
||||
|
||||
private readonly log: Logger;
|
||||
private readonly initializerContext: PluginInitializerContext;
|
||||
|
||||
private instance?: Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
||||
|
||||
constructor(
|
||||
public readonly path: string,
|
||||
public readonly manifest: PluginManifest,
|
||||
private readonly initializerContext: PluginInitializerContext
|
||||
readonly params: {
|
||||
readonly path: string;
|
||||
readonly manifest: PluginManifest;
|
||||
readonly opaqueId: PluginOpaqueId;
|
||||
readonly initializerContext: PluginInitializerContext;
|
||||
}
|
||||
) {
|
||||
this.log = initializerContext.logger.get();
|
||||
this.name = manifest.id;
|
||||
this.configPath = manifest.configPath;
|
||||
this.requiredPlugins = manifest.requiredPlugins;
|
||||
this.optionalPlugins = manifest.optionalPlugins;
|
||||
this.includesServerPlugin = manifest.server;
|
||||
this.includesUiPlugin = manifest.ui;
|
||||
this.path = params.path;
|
||||
this.manifest = params.manifest;
|
||||
this.opaqueId = params.opaqueId;
|
||||
this.initializerContext = params.initializerContext;
|
||||
this.log = params.initializerContext.logger.get();
|
||||
this.name = params.manifest.id;
|
||||
this.configPath = params.manifest.configPath;
|
||||
this.requiredPlugins = params.manifest.requiredPlugins;
|
||||
this.optionalPlugins = params.manifest.optionalPlugins;
|
||||
this.includesServerPlugin = params.manifest.server;
|
||||
this.includesUiPlugin = params.manifest.ui;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,28 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { EnvironmentMode } from '../config';
|
||||
import { CoreContext } from '../core_context';
|
||||
import { LoggerFactory } from '../logging';
|
||||
import { PluginWrapper, PluginManifest } from './plugin';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
|
||||
import { PluginInitializerContext, PluginManifest, PluginOpaqueId } from './types';
|
||||
import { CoreSetup, CoreStart } from '..';
|
||||
|
||||
/**
|
||||
* Context that's available to plugins during initialization stage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PluginInitializerContext<ConfigSchema = unknown> {
|
||||
env: { mode: EnvironmentMode };
|
||||
logger: LoggerFactory;
|
||||
config: {
|
||||
create: <T = ConfigSchema>() => Observable<T>;
|
||||
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns a facade for `CoreContext` that will be exposed to the plugin initializer.
|
||||
* This facade should be safe to use across entire plugin lifespan.
|
||||
|
@ -54,9 +38,12 @@ export interface PluginInitializerContext<ConfigSchema = unknown> {
|
|||
*/
|
||||
export function createPluginInitializerContext(
|
||||
coreContext: CoreContext,
|
||||
opaqueId: PluginOpaqueId,
|
||||
pluginManifest: PluginManifest
|
||||
): PluginInitializerContext {
|
||||
return {
|
||||
opaqueId,
|
||||
|
||||
/**
|
||||
* Environment information that is safe to expose to plugins and may be beneficial for them.
|
||||
*/
|
||||
|
@ -112,6 +99,9 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
plugin: PluginWrapper<TPlugin, TPluginDependencies>
|
||||
): CoreSetup {
|
||||
return {
|
||||
context: {
|
||||
createContextContainer: deps.context.createContextContainer,
|
||||
},
|
||||
elasticsearch: {
|
||||
adminClient$: deps.elasticsearch.adminClient$,
|
||||
dataClient$: deps.elasticsearch.dataClient$,
|
||||
|
|
|
@ -22,6 +22,7 @@ import { PluginsService } from './plugins_service';
|
|||
type ServiceContract = PublicMethodsOf<PluginsService>;
|
||||
const createServiceMock = () => {
|
||||
const mocked: jest.Mocked<ServiceContract> = {
|
||||
discover: jest.fn(),
|
||||
setup: jest.fn(),
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
|
|
|
@ -33,14 +33,17 @@ import { PluginWrapper } from './plugin';
|
|||
import { PluginsService } from './plugins_service';
|
||||
import { PluginsSystem } from './plugins_system';
|
||||
import { config } from './plugins_config';
|
||||
import { contextServiceMock } from '../context/context_service.mock';
|
||||
|
||||
const MockPluginsSystem: jest.Mock<PluginsSystem> = PluginsSystem as any;
|
||||
|
||||
let pluginsService: PluginsService;
|
||||
let configService: ConfigService;
|
||||
let coreId: symbol;
|
||||
let env: Env;
|
||||
let mockPluginSystem: jest.Mocked<PluginsSystem>;
|
||||
const setupDeps = {
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
http: httpServiceMock.createSetupContract(),
|
||||
};
|
||||
|
@ -63,6 +66,7 @@ beforeEach(async () => {
|
|||
},
|
||||
};
|
||||
|
||||
coreId = Symbol('core');
|
||||
env = Env.createDefault(getEnvOptions());
|
||||
|
||||
configService = new ConfigService(
|
||||
|
@ -71,7 +75,7 @@ beforeEach(async () => {
|
|||
logger
|
||||
);
|
||||
await configService.setSchema(config.path, config.schema);
|
||||
pluginsService = new PluginsService({ env, logger, configService });
|
||||
pluginsService = new PluginsService({ coreId, env, logger, configService });
|
||||
|
||||
[mockPluginSystem] = MockPluginsSystem.mock.instances as any;
|
||||
});
|
||||
|
@ -80,13 +84,13 @@ afterEach(() => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('`setup` throws if plugin has an invalid manifest', async () => {
|
||||
test('`discover` throws if plugin has an invalid manifest', async () => {
|
||||
mockDiscover.mockReturnValue({
|
||||
error$: from([PluginDiscoveryError.invalidManifest('path-1', new Error('Invalid JSON'))]),
|
||||
plugin$: from([]),
|
||||
});
|
||||
|
||||
await expect(pluginsService.setup(setupDeps)).rejects.toMatchInlineSnapshot(`
|
||||
await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(`
|
||||
[Error: Failed to initialize plugins:
|
||||
Invalid JSON (invalid-manifest, path-1)]
|
||||
`);
|
||||
|
@ -99,7 +103,7 @@ Array [
|
|||
`);
|
||||
});
|
||||
|
||||
test('`setup` throws if plugin required Kibana version is incompatible with the current version', async () => {
|
||||
test('`discover` throws if plugin required Kibana version is incompatible with the current version', async () => {
|
||||
mockDiscover.mockReturnValue({
|
||||
error$: from([
|
||||
PluginDiscoveryError.incompatibleVersion('path-3', new Error('Incompatible version')),
|
||||
|
@ -107,7 +111,7 @@ test('`setup` throws if plugin required Kibana version is incompatible with the
|
|||
plugin$: from([]),
|
||||
});
|
||||
|
||||
await expect(pluginsService.setup(setupDeps)).rejects.toMatchInlineSnapshot(`
|
||||
await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(`
|
||||
[Error: Failed to initialize plugins:
|
||||
Incompatible version (incompatible-version, path-3)]
|
||||
`);
|
||||
|
@ -120,13 +124,13 @@ Array [
|
|||
`);
|
||||
});
|
||||
|
||||
test('`setup` throws if discovered plugins with conflicting names', async () => {
|
||||
test('`discover` throws if discovered plugins with conflicting names', async () => {
|
||||
mockDiscover.mockReturnValue({
|
||||
error$: from([]),
|
||||
plugin$: from([
|
||||
new PluginWrapper(
|
||||
'path-4',
|
||||
{
|
||||
new PluginWrapper({
|
||||
path: 'path-4',
|
||||
manifest: {
|
||||
id: 'conflicting-id',
|
||||
version: 'some-version',
|
||||
configPath: 'path',
|
||||
|
@ -136,11 +140,12 @@ test('`setup` throws if discovered plugins with conflicting names', async () =>
|
|||
server: true,
|
||||
ui: true,
|
||||
},
|
||||
{ logger } as any
|
||||
),
|
||||
new PluginWrapper(
|
||||
'path-5',
|
||||
{
|
||||
opaqueId: Symbol(),
|
||||
initializerContext: { logger } as any,
|
||||
}),
|
||||
new PluginWrapper({
|
||||
path: 'path-5',
|
||||
manifest: {
|
||||
id: 'conflicting-id',
|
||||
version: 'some-other-version',
|
||||
configPath: ['plugin', 'path'],
|
||||
|
@ -150,12 +155,13 @@ test('`setup` throws if discovered plugins with conflicting names', async () =>
|
|||
server: true,
|
||||
ui: false,
|
||||
},
|
||||
{ logger } as any
|
||||
),
|
||||
opaqueId: Symbol(),
|
||||
initializerContext: { logger } as any,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
await expect(pluginsService.setup(setupDeps)).rejects.toMatchInlineSnapshot(
|
||||
await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Plugin with id "conflicting-id" is already registered!]`
|
||||
);
|
||||
|
||||
|
@ -163,7 +169,7 @@ test('`setup` throws if discovered plugins with conflicting names', async () =>
|
|||
expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('`setup` properly detects plugins that should be disabled.', async () => {
|
||||
test('`discover` properly detects plugins that should be disabled.', async () => {
|
||||
jest
|
||||
.spyOn(configService, 'isEnabledAtPath')
|
||||
.mockImplementation(path => Promise.resolve(!path.includes('disabled')));
|
||||
|
@ -174,9 +180,9 @@ test('`setup` properly detects plugins that should be disabled.', async () => {
|
|||
mockDiscover.mockReturnValue({
|
||||
error$: from([]),
|
||||
plugin$: from([
|
||||
new PluginWrapper(
|
||||
'path-1',
|
||||
{
|
||||
new PluginWrapper({
|
||||
path: 'path-1',
|
||||
manifest: {
|
||||
id: 'explicitly-disabled-plugin',
|
||||
version: 'some-version',
|
||||
configPath: 'path-1-disabled',
|
||||
|
@ -186,11 +192,12 @@ test('`setup` properly detects plugins that should be disabled.', async () => {
|
|||
server: true,
|
||||
ui: true,
|
||||
},
|
||||
{ logger } as any
|
||||
),
|
||||
new PluginWrapper(
|
||||
'path-2',
|
||||
{
|
||||
opaqueId: Symbol(),
|
||||
initializerContext: { logger } as any,
|
||||
}),
|
||||
new PluginWrapper({
|
||||
path: 'path-2',
|
||||
manifest: {
|
||||
id: 'plugin-with-missing-required-deps',
|
||||
version: 'some-version',
|
||||
configPath: 'path-2',
|
||||
|
@ -200,11 +207,12 @@ test('`setup` properly detects plugins that should be disabled.', async () => {
|
|||
server: true,
|
||||
ui: true,
|
||||
},
|
||||
{ logger } as any
|
||||
),
|
||||
new PluginWrapper(
|
||||
'path-3',
|
||||
{
|
||||
opaqueId: Symbol(),
|
||||
initializerContext: { logger } as any,
|
||||
}),
|
||||
new PluginWrapper({
|
||||
path: 'path-3',
|
||||
manifest: {
|
||||
id: 'plugin-with-disabled-transitive-dep',
|
||||
version: 'some-version',
|
||||
configPath: 'path-3',
|
||||
|
@ -214,11 +222,12 @@ test('`setup` properly detects plugins that should be disabled.', async () => {
|
|||
server: true,
|
||||
ui: true,
|
||||
},
|
||||
{ logger } as any
|
||||
),
|
||||
new PluginWrapper(
|
||||
'path-4',
|
||||
{
|
||||
opaqueId: Symbol(),
|
||||
initializerContext: { logger } as any,
|
||||
}),
|
||||
new PluginWrapper({
|
||||
path: 'path-4',
|
||||
manifest: {
|
||||
id: 'another-explicitly-disabled-plugin',
|
||||
version: 'some-version',
|
||||
configPath: 'path-4-disabled',
|
||||
|
@ -228,16 +237,18 @@ test('`setup` properly detects plugins that should be disabled.', async () => {
|
|||
server: true,
|
||||
ui: true,
|
||||
},
|
||||
{ logger } as any
|
||||
),
|
||||
opaqueId: Symbol(),
|
||||
initializerContext: { logger } as any,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
const start = await pluginsService.setup(setupDeps);
|
||||
await pluginsService.discover();
|
||||
const setup = await pluginsService.setup(setupDeps);
|
||||
|
||||
expect(start.contracts).toBeInstanceOf(Map);
|
||||
expect(start.uiPlugins.public).toBeInstanceOf(Map);
|
||||
expect(start.uiPlugins.internal).toBeInstanceOf(Map);
|
||||
expect(setup.contracts).toBeInstanceOf(Map);
|
||||
expect(setup.uiPlugins.public).toBeInstanceOf(Map);
|
||||
expect(setup.uiPlugins.internal).toBeInstanceOf(Map);
|
||||
expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled();
|
||||
expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1);
|
||||
expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps);
|
||||
|
@ -260,10 +271,10 @@ Array [
|
|||
`);
|
||||
});
|
||||
|
||||
test('`setup` properly invokes `discover` and ignores non-critical errors.', async () => {
|
||||
const firstPlugin = new PluginWrapper(
|
||||
'path-1',
|
||||
{
|
||||
test('`discover` properly invokes plugin discovery and ignores non-critical errors.', async () => {
|
||||
const firstPlugin = new PluginWrapper({
|
||||
path: 'path-1',
|
||||
manifest: {
|
||||
id: 'some-id',
|
||||
version: 'some-version',
|
||||
configPath: 'path',
|
||||
|
@ -273,12 +284,13 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy
|
|||
server: true,
|
||||
ui: true,
|
||||
},
|
||||
{ logger } as any
|
||||
);
|
||||
opaqueId: Symbol(),
|
||||
initializerContext: { logger } as any,
|
||||
});
|
||||
|
||||
const secondPlugin = new PluginWrapper(
|
||||
'path-2',
|
||||
{
|
||||
const secondPlugin = new PluginWrapper({
|
||||
path: 'path-2',
|
||||
manifest: {
|
||||
id: 'some-other-id',
|
||||
version: 'some-other-version',
|
||||
configPath: ['plugin', 'path'],
|
||||
|
@ -288,8 +300,9 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy
|
|||
server: true,
|
||||
ui: false,
|
||||
},
|
||||
{ logger } as any
|
||||
);
|
||||
opaqueId: Symbol(),
|
||||
initializerContext: { logger } as any,
|
||||
});
|
||||
|
||||
mockDiscover.mockReturnValue({
|
||||
error$: from([
|
||||
|
@ -300,15 +313,7 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy
|
|||
plugin$: from([firstPlugin, secondPlugin]),
|
||||
});
|
||||
|
||||
const contracts = new Map();
|
||||
const discoveredPlugins = { public: new Map(), internal: new Map() };
|
||||
mockPluginSystem.setupPlugins.mockResolvedValue(contracts);
|
||||
mockPluginSystem.uiPlugins.mockReturnValue(discoveredPlugins);
|
||||
|
||||
const setup = await pluginsService.setup(setupDeps);
|
||||
|
||||
expect(setup.contracts).toBe(contracts);
|
||||
expect(setup.uiPlugins).toBe(discoveredPlugins);
|
||||
await pluginsService.discover();
|
||||
expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
|
||||
expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin);
|
||||
expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin);
|
||||
|
@ -325,7 +330,7 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy
|
|||
resolve(process.cwd(), '..', 'kibana-extra'),
|
||||
],
|
||||
},
|
||||
{ env, logger, configService }
|
||||
{ coreId, env, logger, configService }
|
||||
);
|
||||
|
||||
const logs = loggingServiceMock.collect(logger);
|
||||
|
@ -338,7 +343,7 @@ test('`stop` stops plugins system', async () => {
|
|||
expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('`setup` registers plugin config schema in config service', async () => {
|
||||
test('`discover` registers plugin config schema in config service', async () => {
|
||||
const configSchema = schema.string();
|
||||
jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve());
|
||||
jest.doMock(
|
||||
|
@ -355,9 +360,9 @@ test('`setup` registers plugin config schema in config service', async () => {
|
|||
mockDiscover.mockReturnValue({
|
||||
error$: from([]),
|
||||
plugin$: from([
|
||||
new PluginWrapper(
|
||||
'path-with-schema',
|
||||
{
|
||||
new PluginWrapper({
|
||||
path: 'path-with-schema',
|
||||
manifest: {
|
||||
id: 'some-id',
|
||||
version: 'some-version',
|
||||
configPath: 'path',
|
||||
|
@ -367,10 +372,11 @@ test('`setup` registers plugin config schema in config service', async () => {
|
|||
server: true,
|
||||
ui: true,
|
||||
},
|
||||
{ logger } as any
|
||||
),
|
||||
opaqueId: Symbol(),
|
||||
initializerContext: { logger } as any,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
await pluginsService.setup(setupDeps);
|
||||
await pluginsService.discover();
|
||||
expect(configService.setSchema).toBeCalledWith('path', configSchema);
|
||||
});
|
||||
|
|
|
@ -25,9 +25,11 @@ import { ElasticsearchServiceSetup } from '../elasticsearch/elasticsearch_servic
|
|||
import { HttpServiceSetup } from '../http/http_service';
|
||||
import { Logger } from '../logging';
|
||||
import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery';
|
||||
import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } from './plugin';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName } from './types';
|
||||
import { PluginsConfig, PluginsConfigType } from './plugins_config';
|
||||
import { PluginsSystem } from './plugins_system';
|
||||
import { ContextSetup } from '../context';
|
||||
|
||||
/** @public */
|
||||
export interface PluginsServiceSetup {
|
||||
|
@ -45,6 +47,7 @@ export interface PluginsServiceStart {
|
|||
|
||||
/** @internal */
|
||||
export interface PluginsServiceSetupDeps {
|
||||
context: ContextSetup;
|
||||
elasticsearch: ElasticsearchServiceSetup;
|
||||
http: HttpServiceSetup;
|
||||
}
|
||||
|
@ -66,8 +69,8 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
.pipe(map(rawConfig => new PluginsConfig(rawConfig, coreContext.env)));
|
||||
}
|
||||
|
||||
public async setup(deps: PluginsServiceSetupDeps) {
|
||||
this.log.debug('Setting up plugins service');
|
||||
public async discover() {
|
||||
this.log.debug('Discovering plugins');
|
||||
|
||||
const config = await this.config$.pipe(first()).toPromise();
|
||||
|
||||
|
@ -75,6 +78,15 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
await this.handleDiscoveryErrors(error$);
|
||||
await this.handleDiscoveredPlugins(plugin$);
|
||||
|
||||
// Return dependency tree
|
||||
return this.pluginsSystem.getPluginDependencies();
|
||||
}
|
||||
|
||||
public async setup(deps: PluginsServiceSetupDeps) {
|
||||
this.log.debug('Setting up plugins service');
|
||||
|
||||
const config = await this.config$.pipe(first()).toPromise();
|
||||
|
||||
if (!config.initialize || this.coreContext.env.isDevClusterMaster) {
|
||||
this.log.info('Plugin initialization disabled.');
|
||||
return {
|
||||
|
|
|
@ -31,8 +31,10 @@ import { configServiceMock } from '../config/config_service.mock';
|
|||
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
|
||||
import { httpServiceMock } from '../http/http_service.mock';
|
||||
import { loggingServiceMock } from '../logging/logging_service.mock';
|
||||
import { PluginWrapper, PluginName } from './plugin';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { PluginName } from './types';
|
||||
import { PluginsSystem } from './plugins_system';
|
||||
import { contextServiceMock } from '../context/context_service.mock';
|
||||
|
||||
const logger = loggingServiceMock.create();
|
||||
function createPlugin(
|
||||
|
@ -43,9 +45,9 @@ function createPlugin(
|
|||
server = true,
|
||||
}: { required?: string[]; optional?: string[]; server?: boolean } = {}
|
||||
) {
|
||||
return new PluginWrapper(
|
||||
'some-path',
|
||||
{
|
||||
return new PluginWrapper({
|
||||
path: 'some-path',
|
||||
manifest: {
|
||||
id,
|
||||
version: 'some-version',
|
||||
configPath: 'path',
|
||||
|
@ -55,8 +57,9 @@ function createPlugin(
|
|||
server,
|
||||
ui: true,
|
||||
},
|
||||
{ logger } as any
|
||||
);
|
||||
opaqueId: Symbol(id),
|
||||
initializerContext: { logger } as any,
|
||||
});
|
||||
}
|
||||
|
||||
let pluginsSystem: PluginsSystem;
|
||||
|
@ -65,13 +68,14 @@ configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true }));
|
|||
let env: Env;
|
||||
let coreContext: CoreContext;
|
||||
const setupDeps = {
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
http: httpServiceMock.createSetupContract(),
|
||||
};
|
||||
beforeEach(() => {
|
||||
env = Env.createDefault(getEnvOptions());
|
||||
|
||||
coreContext = { env, logger, configService: configService as any };
|
||||
coreContext = { coreId: Symbol(), env, logger, configService: configService as any };
|
||||
|
||||
pluginsSystem = new PluginsSystem(coreContext);
|
||||
});
|
||||
|
@ -87,6 +91,27 @@ test('can be setup even without plugins', async () => {
|
|||
expect(pluginsSetup.size).toBe(0);
|
||||
});
|
||||
|
||||
test('getPluginDependencies returns dependency tree of symbols', () => {
|
||||
pluginsSystem.addPlugin(createPlugin('plugin-a', { required: ['no-dep'] }));
|
||||
pluginsSystem.addPlugin(
|
||||
createPlugin('plugin-b', { required: ['plugin-a'], optional: ['no-dep', 'other'] })
|
||||
);
|
||||
pluginsSystem.addPlugin(createPlugin('no-dep'));
|
||||
|
||||
expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
Symbol(plugin-a) => Array [
|
||||
Symbol(no-dep),
|
||||
],
|
||||
Symbol(plugin-b) => Array [
|
||||
Symbol(plugin-a),
|
||||
Symbol(no-dep),
|
||||
],
|
||||
Symbol(no-dep) => Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('`setupPlugins` throws plugin has missing required dependency', async () => {
|
||||
pluginsSystem.addPlugin(createPlugin('some-id', { required: ['missing-dep'] }));
|
||||
|
||||
|
|
|
@ -21,7 +21,8 @@ import { pick } from 'lodash';
|
|||
|
||||
import { CoreContext } from '../core_context';
|
||||
import { Logger } from '../logging';
|
||||
import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } from './plugin';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName, PluginOpaqueId } from './types';
|
||||
import { createPluginSetupContext, createPluginStartContext } from './plugin_context';
|
||||
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
|
||||
|
||||
|
@ -40,6 +41,25 @@ export class PluginsSystem {
|
|||
this.plugins.set(plugin.name, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a ReadonlyMap of each plugin and an Array of its available dependencies
|
||||
* @internal
|
||||
*/
|
||||
public getPluginDependencies(): ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]> {
|
||||
// Return dependency map of opaque ids
|
||||
return new Map(
|
||||
[...this.plugins].map(([name, plugin]) => [
|
||||
plugin.opaqueId,
|
||||
[
|
||||
...new Set([
|
||||
...plugin.requiredPlugins,
|
||||
...plugin.optionalPlugins.filter(optPlugin => this.plugins.has(optPlugin)),
|
||||
]),
|
||||
].map(depId => this.plugins.get(depId)!.opaqueId),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public async setupPlugins(deps: PluginsServiceSetupDeps) {
|
||||
const contracts = new Map<PluginName, unknown>();
|
||||
if (this.plugins.size === 0) {
|
||||
|
|
175
src/core/server/plugins/types.ts
Normal file
175
src/core/server/plugins/types.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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 { Observable } from 'rxjs';
|
||||
import { Type } from '@kbn/config-schema';
|
||||
|
||||
import { ConfigPath, EnvironmentMode } from '../config';
|
||||
import { LoggerFactory } from '../logging';
|
||||
import { CoreSetup, CoreStart } from '..';
|
||||
|
||||
export type PluginConfigSchema = Type<unknown> | null;
|
||||
|
||||
/**
|
||||
* Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays
|
||||
* that use it as a key or value more obvious.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PluginName = string;
|
||||
|
||||
/** @public */
|
||||
export type PluginOpaqueId = symbol;
|
||||
|
||||
/**
|
||||
* Describes the set of required and optional properties plugin can define in its
|
||||
* mandatory JSON manifest file.
|
||||
* @internal
|
||||
*/
|
||||
export interface PluginManifest {
|
||||
/**
|
||||
* Identifier of the plugin.
|
||||
*/
|
||||
readonly id: PluginName;
|
||||
|
||||
/**
|
||||
* Version of the plugin.
|
||||
*/
|
||||
readonly version: string;
|
||||
|
||||
/**
|
||||
* The version of Kibana the plugin is compatible with, defaults to "version".
|
||||
*/
|
||||
readonly kibanaVersion: string;
|
||||
|
||||
/**
|
||||
* Root configuration path used by the plugin, defaults to "id".
|
||||
*/
|
||||
readonly configPath: ConfigPath;
|
||||
|
||||
/**
|
||||
* An optional list of the other plugins that **must be** installed and enabled
|
||||
* for this plugin to function properly.
|
||||
*/
|
||||
readonly requiredPlugins: readonly PluginName[];
|
||||
|
||||
/**
|
||||
* An optional list of the other plugins that if installed and enabled **may be**
|
||||
* leveraged by this plugin for some additional functionality but otherwise are
|
||||
* not required for this plugin to work properly.
|
||||
*/
|
||||
readonly optionalPlugins: readonly PluginName[];
|
||||
|
||||
/**
|
||||
* Specifies whether plugin includes some client/browser specific functionality
|
||||
* that should be included into client bundle via `public/ui_plugin.js` file.
|
||||
*/
|
||||
readonly ui: boolean;
|
||||
|
||||
/**
|
||||
* Specifies whether plugin includes some server-side specific functionality.
|
||||
*/
|
||||
readonly server: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Small container object used to expose information about discovered plugins that may
|
||||
* or may not have been started.
|
||||
* @public
|
||||
*/
|
||||
export interface DiscoveredPlugin {
|
||||
/**
|
||||
* Identifier of the plugin.
|
||||
*/
|
||||
readonly id: PluginName;
|
||||
|
||||
/**
|
||||
* Root configuration path used by the plugin, defaults to "id".
|
||||
*/
|
||||
readonly configPath: ConfigPath;
|
||||
|
||||
/**
|
||||
* An optional list of the other plugins that **must be** installed and enabled
|
||||
* for this plugin to function properly.
|
||||
*/
|
||||
readonly requiredPlugins: readonly PluginName[];
|
||||
|
||||
/**
|
||||
* An optional list of the other plugins that if installed and enabled **may be**
|
||||
* leveraged by this plugin for some additional functionality but otherwise are
|
||||
* not required for this plugin to work properly.
|
||||
*/
|
||||
readonly optionalPlugins: readonly PluginName[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An extended `DiscoveredPlugin` that exposes more sensitive information. Should never
|
||||
* be exposed to client-side code.
|
||||
* @internal
|
||||
*/
|
||||
export interface DiscoveredPluginInternal extends DiscoveredPlugin {
|
||||
/**
|
||||
* Path on the filesystem where plugin was loaded from.
|
||||
*/
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface that should be returned by a `PluginInitializer`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface Plugin<
|
||||
TSetup = void,
|
||||
TStart = void,
|
||||
TPluginsSetup extends object = object,
|
||||
TPluginsStart extends object = object
|
||||
> {
|
||||
setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
|
||||
start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;
|
||||
stop?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context that's available to plugins during initialization stage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PluginInitializerContext<ConfigSchema = unknown> {
|
||||
opaqueId: PluginOpaqueId;
|
||||
env: { mode: EnvironmentMode };
|
||||
logger: LoggerFactory;
|
||||
config: {
|
||||
create: <T = ConfigSchema>() => Observable<T>;
|
||||
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The `plugin` export at the root of a plugin's `server` directory should conform
|
||||
* to this interface.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PluginInitializer<
|
||||
TSetup,
|
||||
TStart,
|
||||
TPluginsSetup extends object = object,
|
||||
TPluginsStart extends object = object
|
||||
> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
|
@ -92,8 +92,24 @@ export class ConfigService {
|
|||
setSchema(path: ConfigPath, schema: Type<unknown>): Promise<void>;
|
||||
}
|
||||
|
||||
// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "kibana" does not have an export "IContextContainer"
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface ContextSetup {
|
||||
// Warning: (ae-forgotten-export) The symbol "IContextContainer" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "IContextContainer"
|
||||
createContextContainer<TContext extends {}, THandlerReturn, THandlerParmaters extends any[] = []>(): IContextContainer<TContext, THandlerReturn, THandlerParmaters>;
|
||||
}
|
||||
|
||||
// @internal (undocumented)
|
||||
export type CoreId = symbol;
|
||||
|
||||
// @public
|
||||
export interface CoreSetup {
|
||||
// (undocumented)
|
||||
context: {
|
||||
createContextContainer: ContextSetup['createContextContainer'];
|
||||
};
|
||||
// (undocumented)
|
||||
elasticsearch: {
|
||||
adminClient$: Observable<ClusterClient>;
|
||||
|
@ -438,11 +454,16 @@ export interface PluginInitializerContext<ConfigSchema = unknown> {
|
|||
};
|
||||
// (undocumented)
|
||||
logger: LoggerFactory;
|
||||
// (undocumented)
|
||||
opaqueId: PluginOpaqueId;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type PluginName = string;
|
||||
|
||||
// @public (undocumented)
|
||||
export type PluginOpaqueId = symbol;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface PluginsServiceSetup {
|
||||
// (undocumented)
|
||||
|
@ -995,7 +1016,7 @@ export interface SessionStorageFactory<T> {
|
|||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/core/server/http/router/response.ts:188:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/core/server/plugins/plugin_context.ts:34:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts
|
||||
// src/core/server/plugins/plugins_service.ts:37:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts
|
||||
// src/core/server/plugins/plugins_service.ts:39:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts
|
||||
// src/core/server/plugins/types.ts:156:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts
|
||||
|
||||
```
|
||||
|
|
|
@ -31,9 +31,11 @@ import { config as httpConfig } from './http';
|
|||
import { config as loggingConfig } from './logging';
|
||||
import { config as devConfig } from './dev';
|
||||
import { mapToObject } from '../utils/';
|
||||
import { ContextService } from './context';
|
||||
|
||||
export class Server {
|
||||
public readonly configService: ConfigService;
|
||||
private readonly context: ContextService;
|
||||
private readonly elasticsearch: ElasticsearchService;
|
||||
private readonly http: HttpService;
|
||||
private readonly plugins: PluginsService;
|
||||
|
@ -48,7 +50,8 @@ export class Server {
|
|||
this.log = this.logger.get('server');
|
||||
this.configService = new ConfigService(config$, env, logger);
|
||||
|
||||
const core = { configService: this.configService, env, logger };
|
||||
const core = { coreId: Symbol('core'), configService: this.configService, env, logger };
|
||||
this.context = new ContextService(core);
|
||||
this.http = new HttpService(core);
|
||||
this.plugins = new PluginsService(core);
|
||||
this.legacy = new LegacyService(core);
|
||||
|
@ -58,19 +61,25 @@ export class Server {
|
|||
public async setup() {
|
||||
this.log.debug('setting up server');
|
||||
|
||||
// Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph.
|
||||
const pluginDependencies = await this.plugins.discover();
|
||||
|
||||
const httpSetup = await this.http.setup();
|
||||
this.registerDefaultRoute(httpSetup);
|
||||
|
||||
const contextServiceSetup = this.context.setup({ pluginDependencies });
|
||||
const elasticsearchServiceSetup = await this.elasticsearch.setup({
|
||||
http: httpSetup,
|
||||
});
|
||||
|
||||
const pluginsSetup = await this.plugins.setup({
|
||||
context: contextServiceSetup,
|
||||
elasticsearch: elasticsearchServiceSetup,
|
||||
http: httpSetup,
|
||||
});
|
||||
|
||||
const coreSetup = {
|
||||
context: contextServiceSetup,
|
||||
elasticsearch: elasticsearchServiceSetup,
|
||||
http: httpSetup,
|
||||
plugins: pluginsSetup,
|
||||
|
|
22
src/core/server/types.ts
Normal file
22
src/core/server/types.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/** This module is intended for consumption by public to avoid import issues with server-side code */
|
||||
|
||||
export { PluginOpaqueId } from './plugins/types';
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { ContextContainer } from './context';
|
||||
import { PluginOpaqueId } from '../plugins';
|
||||
import { PluginOpaqueId } from '../server';
|
||||
|
||||
const pluginA = Symbol('pluginA');
|
||||
const pluginB = Symbol('pluginB');
|
|
@ -18,9 +18,8 @@
|
|||
*/
|
||||
|
||||
import { flatten } from 'lodash';
|
||||
import { pick } from '../../utils';
|
||||
import { CoreId } from '../core_system';
|
||||
import { PluginOpaqueId } from '../plugins';
|
||||
import { pick } from '.';
|
||||
import { CoreId, PluginOpaqueId } from '../server';
|
||||
|
||||
/**
|
||||
* A function that returns a context value for a specific key of given context type.
|
|
@ -17,9 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './assert_never';
|
||||
export * from './context';
|
||||
export * from './deep_freeze';
|
||||
export * from './get';
|
||||
export * from './map_to_object';
|
||||
export * from './pick';
|
||||
export * from './assert_never';
|
||||
export * from './url';
|
||||
export * from './deep_freeze';
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"../typings/lodash.topath/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"plugin_functional/plugins/**/*"
|
||||
|
|
Loading…
Reference in a new issue