Add ContextService to server (#42395)

This commit is contained in:
Josh Dover 2019-08-06 12:24:49 -05:00 committed by GitHub
parent 98b445a6e0
commit 5192dac0b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1028 additions and 369 deletions

View file

@ -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
}));

View file

@ -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) |

View file

@ -0,0 +1,12 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md)
## PluginOpaqueId type
<b>Signature:</b>
```typescript
export declare type PluginOpaqueId = symbol;
```

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ContextSetup](./kibana-plugin-server.contextsetup.md) &gt; [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>`

View file

@ -0,0 +1,78 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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);
}
};
}
}
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CoreSetup](./kibana-plugin-server.coresetup.md) &gt; [context](./kibana-plugin-server.coresetup.context.md)
## CoreSetup.context property
<b>Signature:</b>
```typescript
context: {
createContextContainer: ContextSetup['createContextContainer'];
};
```

View file

@ -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&lt;ClusterClient&gt;;</code><br/><code> dataClient$: Observable&lt;ClusterClient&gt;;</code><br/><code> createClient: (type: string, clientConfig?: Partial&lt;ElasticsearchClientConfig&gt;) =&gt; 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> | |

View file

@ -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. |

View file

@ -19,4 +19,5 @@ export interface PluginInitializerContext<ConfigSchema = unknown>
| [config](./kibana-plugin-server.plugininitializercontext.config.md) | <code>{</code><br/><code> create: &lt;T = ConfigSchema&gt;() =&gt; Observable&lt;T&gt;;</code><br/><code> createIfExists: &lt;T = ConfigSchema&gt;() =&gt; Observable&lt;T &#124; undefined&gt;;</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> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) &gt; [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md)
## PluginInitializerContext.opaqueId property
<b>Signature:</b>
```typescript
opaqueId: PluginOpaqueId;
```

View file

@ -0,0 +1,12 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md)
## PluginOpaqueId type
<b>Signature:</b>
```typescript
export declare type PluginOpaqueId = symbol;
```

View file

@ -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> = {

View file

@ -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,
}));

View file

@ -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[]>();

View file

@ -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
* }));

View file

@ -18,4 +18,4 @@
*/
export { ContextService, ContextSetup } from './context_service';
export { IContextContainer, IContextProvider, IContextHandler } from './context';
export { IContextContainer, IContextProvider, IContextHandler } from '../../utils/context';

View file

@ -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;

View file

@ -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,

View file

@ -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';

View file

@ -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`.
*

View file

@ -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 '../';

View file

@ -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,

View file

@ -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)

View 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,
};

View 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,
}));

View 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);
});
});
});

View 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>;
}

View 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';

View file

@ -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;

View file

@ -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);
});

View file

@ -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,

View file

@ -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,
};

View file

@ -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."`
);

View file

@ -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(),
};

View file

@ -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);

View file

@ -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,

View file

@ -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])
);

View file

@ -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';

View file

@ -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"`
);

View file

@ -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;
}
/**

View file

@ -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$,

View file

@ -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(),

View file

@ -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);
});

View file

@ -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 {

View file

@ -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'] }));

View file

@ -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) {

View 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>;

View file

@ -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
```

View file

@ -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
View 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';

View file

@ -18,7 +18,7 @@
*/
import { ContextContainer } from './context';
import { PluginOpaqueId } from '../plugins';
import { PluginOpaqueId } from '../server';
const pluginA = Symbol('pluginA');
const pluginB = Symbol('pluginB');

View file

@ -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.

View file

@ -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';

View file

@ -13,6 +13,7 @@
"include": [
"**/*.ts",
"**/*.tsx",
"../typings/lodash.topath/*.ts",
],
"exclude": [
"plugin_functional/plugins/**/*"