Skip to content

Commit

Permalink
Implements getStartServices on server-side (#55156)
Browse files Browse the repository at this point in the history
* implements server-side getStartServices

* add unit test

* add integration test

* update generated doc

* improve test
  • Loading branch information
pgayvallet committed Jan 20, 2020
1 parent c4d088f commit 3634d28
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -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]>`

58 changes: 32 additions & 26 deletions docs/development/core/server/kibana-plugin-server.coresetup.md
Original file line number Diff line number Diff line change
@@ -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. |

9 changes: 8 additions & 1 deletion src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export interface RequestHandlerContext {
*
* @public
*/
export interface CoreSetup {
export interface CoreSetup<TPluginsStart extends object = object> {
/** {@link CapabilitiesSetup} */
capabilities: CapabilitiesSetup;
/** {@link ContextSetup} */
Expand All @@ -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]>;
}

/**
Expand Down
12 changes: 7 additions & 5 deletions src/core/server/legacy/legacy_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion src/core/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']> = {
Expand All @@ -105,14 +107,17 @@ function createCoreSetupMock() {
const uiSettingsMock = {
register: uiSettingsServiceMock.createSetupContract().register,
};
const mock: MockedKeys<CoreSetup> = {
const mock: CoreSetupMockType = {
capabilities: capabilitiesServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
elasticsearch: elasticsearchServiceMock.createSetup(),
http: httpMock,
savedObjects: savedObjectsServiceMock.createSetupContract(),
uiSettings: uiSettingsMock,
uuid: uuidServiceMock.createSetupContract(),
getStartServices: jest
.fn<Promise<[ReturnType<typeof createCoreStartMock>, object]>, []>()
.mockResolvedValue([createCoreStartMock(), {}]),
};

return mock;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }));
167 changes: 167 additions & 0 deletions src/core/server/plugins/integration_tests/plugins_service.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
37 changes: 37 additions & 0 deletions src/core/server/plugins/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 3634d28

Please sign in to comment.