Implements getStartServices on server-side (#55156) (#55290)

* implements server-side getStartServices

* add unit test

* add integration test

* update generated doc

* improve test
This commit is contained in:
Pierre Gayvallet 2020-01-20 13:13:56 +01:00 committed by GitHub
parent c4d088f58e
commit 8fba124795
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 316 additions and 40 deletions

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; [CoreSetup](./kibana-plugin-server.coresetup.md) &gt; [getStartServices](./kibana-plugin-server.coresetup.getstartservices.md)
## CoreSetup.getStartServices() method
Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed `start`<!-- -->. This should only be used inside handlers registered during `setup` that will only be executed after `start` lifecycle.
<b>Signature:</b>
```typescript
getStartServices(): Promise<[CoreStart, TPluginsStart]>;
```
<b>Returns:</b>
`Promise<[CoreStart, TPluginsStart]>`

View file

@ -1,26 +1,32 @@
<!-- 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)
## CoreSetup interface
Context passed to the plugins `setup` method.
<b>Signature:</b>
```typescript
export interface CoreSetup
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [capabilities](./kibana-plugin-server.coresetup.capabilities.md) | <code>CapabilitiesSetup</code> | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) |
| [context](./kibana-plugin-server.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-server.contextsetup.md) |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>HttpServiceSetup</code> | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) |
| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | <code>SavedObjectsServiceSetup</code> | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) |
| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | <code>UiSettingsServiceSetup</code> | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) |
| [uuid](./kibana-plugin-server.coresetup.uuid.md) | <code>UuidServiceSetup</code> | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) |
<!-- 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)
## CoreSetup interface
Context passed to the plugins `setup` method.
<b>Signature:</b>
```typescript
export interface CoreSetup<TPluginsStart extends object = object>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [capabilities](./kibana-plugin-server.coresetup.capabilities.md) | <code>CapabilitiesSetup</code> | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) |
| [context](./kibana-plugin-server.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-server.contextsetup.md) |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>HttpServiceSetup</code> | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) |
| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | <code>SavedObjectsServiceSetup</code> | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) |
| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | <code>UiSettingsServiceSetup</code> | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) |
| [uuid](./kibana-plugin-server.coresetup.uuid.md) | <code>UuidServiceSetup</code> | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) |
## Methods
| Method | Description |
| --- | --- |
| [getStartServices()](./kibana-plugin-server.coresetup.getstartservices.md) | Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed <code>start</code>. This should only be used inside handlers registered during <code>setup</code> that will only be executed after <code>start</code> lifecycle. |

View file

@ -283,7 +283,7 @@ export interface RequestHandlerContext {
*
* @public
*/
export interface CoreSetup {
export interface CoreSetup<TPluginsStart extends object = object> {
/** {@link CapabilitiesSetup} */
capabilities: CapabilitiesSetup;
/** {@link ContextSetup} */
@ -298,6 +298,13 @@ export interface CoreSetup {
uiSettings: UiSettingsServiceSetup;
/** {@link UuidServiceSetup} */
uuid: UuidServiceSetup;
/**
* Allows plugins to get access to APIs available in start inside async handlers.
* Promise will not resolve until Core and plugin dependencies have completed `start`.
* This should only be used inside handlers registered during `setup` that will only be executed
* after `start` lifecycle.
*/
getStartServices(): Promise<[CoreStart, TPluginsStart]>;
}
/**

View file

@ -256,6 +256,12 @@ export class LegacyService implements CoreService {
startDeps: LegacyServiceStartDeps,
legacyPlugins: LegacyPlugins
) {
const coreStart: CoreStart = {
capabilities: startDeps.core.capabilities,
savedObjects: { getScopedClient: startDeps.core.savedObjects.getScopedClient },
uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient },
};
const coreSetup: CoreSetup = {
capabilities: setupDeps.core.capabilities,
context: setupDeps.core.context,
@ -291,11 +297,7 @@ export class LegacyService implements CoreService {
uuid: {
getInstanceUuid: setupDeps.core.uuid.getInstanceUuid,
},
};
const coreStart: CoreStart = {
capabilities: startDeps.core.capabilities,
savedObjects: { getScopedClient: startDeps.core.savedObjects.getScopedClient },
uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient },
getStartServices: () => Promise.resolve([coreStart, startDeps.plugins]),
};
// eslint-disable-next-line @typescript-eslint/no-var-requires

View file

@ -86,6 +86,8 @@ function pluginInitializerContextMock<T>(config: T = {} as T) {
return mock;
}
type CoreSetupMockType = MockedKeys<CoreSetup> & jest.Mocked<Pick<CoreSetup, 'getStartServices'>>;
function createCoreSetupMock() {
const httpService = httpServiceMock.createSetupContract();
const httpMock: jest.Mocked<CoreSetup['http']> = {
@ -105,7 +107,7 @@ function createCoreSetupMock() {
const uiSettingsMock = {
register: uiSettingsServiceMock.createSetupContract().register,
};
const mock: MockedKeys<CoreSetup> = {
const mock: CoreSetupMockType = {
capabilities: capabilitiesServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
elasticsearch: elasticsearchServiceMock.createSetup(),
@ -113,6 +115,9 @@ function createCoreSetupMock() {
savedObjects: savedObjectsServiceMock.createSetupContract(),
uiSettings: uiSettingsMock,
uuid: uuidServiceMock.createSetupContract(),
getStartServices: jest
.fn<Promise<[ReturnType<typeof createCoreStartMock>, object]>, []>()
.mockResolvedValue([createCoreStartMock(), {}]),
};
return mock;

View file

@ -0,0 +1,27 @@
/*
* 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 const mockPackage = new Proxy(
{ raw: { __dirname: '/tmp' } as any },
{ get: (obj, prop) => obj.raw[prop] }
);
jest.mock('../../../../core/server/utils/package_json', () => ({ pkg: mockPackage }));
export const mockDiscover = jest.fn();
jest.mock('../discovery/plugins_discovery', () => ({ discover: mockDiscover }));

View file

@ -0,0 +1,167 @@
/*
* 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 { mockPackage, mockDiscover } from './plugins_service.test.mocks';
import { join } from 'path';
import { PluginsService } from '../plugins_service';
import { ConfigPath, ConfigService, Env } from '../../config';
import { getEnvOptions } from '../../config/__mocks__/env';
import { BehaviorSubject, from } from 'rxjs';
import { rawConfigServiceMock } from '../../config/raw_config_service.mock';
import { config } from '../plugins_config';
import { loggingServiceMock } from '../../logging/logging_service.mock';
import { coreMock } from '../../mocks';
import { Plugin } from '../types';
import { PluginWrapper } from '../plugin';
describe('PluginsService', () => {
const logger = loggingServiceMock.create();
let pluginsService: PluginsService;
const createPlugin = (
id: string,
{
path = id,
disabled = false,
version = 'some-version',
requiredPlugins = [],
optionalPlugins = [],
kibanaVersion = '7.0.0',
configPath = [path],
server = true,
ui = true,
}: {
path?: string;
disabled?: boolean;
version?: string;
requiredPlugins?: string[];
optionalPlugins?: string[];
kibanaVersion?: string;
configPath?: ConfigPath;
server?: boolean;
ui?: boolean;
}
): PluginWrapper => {
return new PluginWrapper({
path,
manifest: {
id,
version,
configPath: `${configPath}${disabled ? '-disabled' : ''}`,
kibanaVersion,
requiredPlugins,
optionalPlugins,
server,
ui,
},
opaqueId: Symbol(id),
initializerContext: { logger } as any,
});
};
beforeEach(async () => {
mockPackage.raw = {
branch: 'feature-v1',
version: 'v1',
build: {
distributable: true,
number: 100,
sha: 'feature-v1-build-sha',
},
};
const env = Env.createDefault(getEnvOptions());
const config$ = new BehaviorSubject<Record<string, any>>({
plugins: {
initialize: true,
},
});
const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ });
const configService = new ConfigService(rawConfigService, env, logger);
await configService.setSchema(config.path, config.schema);
pluginsService = new PluginsService({
coreId: Symbol('core'),
env,
logger,
configService,
});
});
it("properly resolves `getStartServices` in plugin's lifecycle", async () => {
expect.assertions(5);
const pluginPath = 'plugin-path';
mockDiscover.mockReturnValue({
error$: from([]),
plugin$: from([
createPlugin('plugin-id', {
path: pluginPath,
configPath: 'path',
}),
]),
});
let startDependenciesResolved = false;
let contextFromStart: any = null;
let contextFromStartService: any = null;
const pluginInitializer = () =>
({
setup: async (coreSetup, deps) => {
coreSetup.getStartServices().then(([core, plugins]) => {
startDependenciesResolved = true;
contextFromStartService = { core, plugins };
});
},
start: async (core, plugins) => {
contextFromStart = { core, plugins };
await new Promise(resolve => setTimeout(resolve, 10));
expect(startDependenciesResolved).toBe(false);
},
} as Plugin);
jest.doMock(
join(pluginPath, 'server'),
() => ({
plugin: pluginInitializer,
}),
{
virtual: true,
}
);
await pluginsService.discover();
const setupDeps = coreMock.createInternalSetup();
await pluginsService.setup(setupDeps);
expect(startDependenciesResolved).toBe(false);
const startDeps = coreMock.createInternalStart();
await pluginsService.start(startDeps);
expect(startDependenciesResolved).toBe(true);
expect(contextFromStart!.core).toEqual(contextFromStartService!.core);
expect(contextFromStart!.plugins).toEqual(contextFromStartService!.plugins);
});
});

View file

@ -237,6 +237,43 @@ test('`start` calls plugin.start with context and dependencies', async () => {
expect(mockPluginInstance.start).toHaveBeenCalledWith(context, deps);
});
test("`start` resolves `startDependencies` Promise after plugin's start", async () => {
expect.assertions(2);
const manifest = createPluginManifest();
const opaqueId = Symbol();
const plugin = new PluginWrapper({
path: 'plugin-with-initializer-path',
manifest,
opaqueId,
initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest),
});
const startContext = { any: 'thing' } as any;
const pluginDeps = { someDep: 'value' };
let startDependenciesResolved = false;
const mockPluginInstance = {
setup: jest.fn(),
start: async () => {
// delay to ensure startDependencies is not resolved until after the plugin instance's start resolves.
await new Promise(resolve => setTimeout(resolve, 10));
expect(startDependenciesResolved).toBe(false);
},
};
mockPluginInitializer.mockReturnValue(mockPluginInstance);
await plugin.setup({} as any, {} as any);
const startDependenciesCheck = plugin.startDependencies.then(resolvedStartDeps => {
startDependenciesResolved = true;
expect(resolvedStartDeps).toEqual([startContext, pluginDeps]);
});
await plugin.start(startContext, pluginDeps);
await startDependenciesCheck;
});
test('`stop` fails if plugin is not set up', async () => {
const manifest = createPluginManifest();
const opaqueId = Symbol();

View file

@ -19,7 +19,8 @@
import { join } from 'path';
import typeDetect from 'type-detect';
import { Subject } from 'rxjs';
import { first } from 'rxjs/operators';
import { Type } from '@kbn/config-schema';
import { Logger } from '../logging';
@ -60,6 +61,9 @@ export class PluginWrapper<
private instance?: Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart]>();
public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise();
constructor(
public readonly params: {
readonly path: string;
@ -88,12 +92,12 @@ export class PluginWrapper<
* @param plugins The dictionary where the key is the dependency name and the value
* is the contract returned by the dependency's `setup` function.
*/
public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) {
public async setup(setupContext: CoreSetup<TPluginsStart>, plugins: TPluginsSetup) {
this.instance = this.createPluginInstance();
this.log.info('Setting up plugin');
return await this.instance.setup(setupContext, plugins);
return this.instance.setup(setupContext, plugins);
}
/**
@ -108,7 +112,9 @@ export class PluginWrapper<
throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`);
}
return await this.instance.start(startContext, plugins);
const startContract = await this.instance.start(startContext, plugins);
this.startDependencies$.next([startContext, plugins]);
return startContract;
}
/**

View file

@ -176,6 +176,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
uuid: {
getInstanceUuid: deps.uuid.getInstanceUuid,
},
getStartServices: () => plugin.startDependencies,
};
}

View file

@ -552,13 +552,14 @@ export interface ContextSetup {
export type CoreId = symbol;
// @public
export interface CoreSetup {
export interface CoreSetup<TPluginsStart extends object = object> {
// (undocumented)
capabilities: CapabilitiesSetup;
// (undocumented)
context: ContextSetup;
// (undocumented)
elasticsearch: ElasticsearchServiceSetup;
getStartServices(): Promise<[CoreStart, TPluginsStart]>;
// (undocumented)
http: HttpServiceSetup;
// (undocumented)

View file

@ -6,7 +6,7 @@
import { of } from 'rxjs';
import { ByteSizeValue } from '@kbn/config-schema';
import { ICustomClusterClient, CoreSetup } from '../../../../src/core/server';
import { ICustomClusterClient } from '../../../../src/core/server';
import { elasticsearchClientPlugin } from './elasticsearch_client_plugin';
import { Plugin, PluginSetupDependencies } from './plugin';
@ -14,7 +14,7 @@ import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/
describe('Security Plugin', () => {
let plugin: Plugin;
let mockCoreSetup: MockedKeys<CoreSetup>;
let mockCoreSetup: ReturnType<typeof coreMock.createSetup>;
let mockClusterClient: jest.Mocked<ICustomClusterClient>;
let mockDependencies: PluginSetupDependencies;
beforeEach(() => {