[New platform] Introduce start phase for core services on server (#35297)

* introduce start phase. setup is bloated with start functionality

* fix amp typings: server is part of start contract now

* update mock files

* root.start(): necessary to run test server

* expose  setup&start server api to simplify testing

* move tests to the new API

* test servers also should call root.start()

* update docs

* update snapshots: this functionality is tested in http server

* split setup/start phases

* update docs

* expose http server if it not started

to get rid of Optional<HttpServer> type and make it Require<HttpServer>

* adopt test to exposed Http server via SetupContract

* udpate docs

* cleanup apm changees

* check legacy service setup before start

* check http server setup before start

* restrict server options mutation; unify Promise interface for setup

* introduce start pahse for plugins service for parity with client side

* Revert "introduce start pahse for plugins service for parity with client side"

This reverts commit c04fdd2e26.
This commit is contained in:
Mikhail Shustov 2019-04-30 13:22:33 +02:00 committed by GitHub
parent 2cc0b21566
commit 39091e7037
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 623 additions and 331 deletions

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md)
## AuthenticationHandler type

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md)
## AuthToolkit.authenticated property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md)
## AuthToolkit interface

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [redirected](./kibana-plugin-server.authtoolkit.redirected.md)
## AuthToolkit.redirected property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [rejected](./kibana-plugin-server.authtoolkit.rejected.md)
## AuthToolkit.rejected property

View file

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

View file

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CoreStart](./kibana-plugin-server.corestart.md)
## CoreStart interface
<b>Signature:</b>
```typescript
export interface CoreStart
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [http](./kibana-plugin-server.corestart.http.md) | <code>HttpServiceStart</code> | |

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md)
## HttpServiceSetup type
@ -6,5 +8,5 @@
<b>Signature:</b>
```typescript
export declare type HttpServiceSetup = HttpServerInfo;
export declare type HttpServiceSetup = HttpServerSetup;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) &gt; [isListening](./kibana-plugin-server.httpservicestart.islistening.md)
## HttpServiceStart.isListening property
Indicates if http server is listening on a port
<b>Signature:</b>
```typescript
isListening: () => boolean;
```

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [HttpServiceStart](./kibana-plugin-server.httpservicestart.md)
## HttpServiceStart interface
<b>Signature:</b>
```typescript
export interface HttpServiceStart
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [isListening](./kibana-plugin-server.httpservicestart.islistening.md) | <code>() =&gt; boolean</code> | Indicates if http server is listening on a port |

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [body](./kibana-plugin-server.kibanarequest.body.md)
## KibanaRequest.body property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [from](./kibana-plugin-server.kibanarequest.from.md)
## KibanaRequest.from() method

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [getFilteredHeaders](./kibana-plugin-server.kibanarequest.getfilteredheaders.md)
## KibanaRequest.getFilteredHeaders() method

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [headers](./kibana-plugin-server.kibanarequest.headers.md)
## KibanaRequest.headers property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md)
## KibanaRequest class

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [params](./kibana-plugin-server.kibanarequest.params.md)
## KibanaRequest.params property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [path](./kibana-plugin-server.kibanarequest.path.md)
## KibanaRequest.path property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [query](./kibana-plugin-server.kibanarequest.query.md)
## KibanaRequest.query property

View file

@ -21,7 +21,9 @@
| [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. |
| [CoreSetup](./kibana-plugin-server.coresetup.md) | |
| [CoreStart](./kibana-plugin-server.corestart.md) | |
| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | |
| [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | |
| [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. |
| [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of <code>LoggerFactory</code> interface is to define a way to retrieve a context-based logger instance. |
| [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata |

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md)
## OnRequestHandler type

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md)
## OnRequestToolkit interface

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) &gt; [next](./kibana-plugin-server.onrequesttoolkit.next.md)
## OnRequestToolkit.next property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) &gt; [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md)
## OnRequestToolkit.redirected property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) &gt; [rejected](./kibana-plugin-server.onrequesttoolkit.rejected.md)
## OnRequestToolkit.rejected property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) &gt; [http](./kibana-plugin-server.pluginsetupcontext.http.md)
## PluginSetupContext.http property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [delete](./kibana-plugin-server.router.delete.md)
## Router.delete() method

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [get](./kibana-plugin-server.router.get.md)
## Router.get() method

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [getRoutes](./kibana-plugin-server.router.getroutes.md)
## Router.getRoutes() method

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md)
## Router class

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [path](./kibana-plugin-server.router.path.md)
## Router.path property

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [post](./kibana-plugin-server.router.post.md)
## Router.post() method

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [put](./kibana-plugin-server.router.put.md)
## Router.put() method

View file

@ -1,3 +1,5 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [routes](./kibana-plugin-server.router.routes.md)
## Router.routes property

View file

@ -37,6 +37,9 @@ export type PluginsServiceStartDeps = CoreStart;
export interface PluginsServiceSetup {
contracts: Map<string, unknown>;
}
export interface PluginsServiceStart {
contracts: Map<string, unknown>;
}
/**
* Service responsible for loading plugin bundles, initializing plugins, and managing the lifecycle
@ -44,7 +47,7 @@ export interface PluginsServiceSetup {
*
* @internal
*/
export class PluginsService implements CoreService<PluginsServiceSetup> {
export class PluginsService implements CoreService<PluginsServiceSetup, PluginsServiceStart> {
/** Plugin wrappers in topological order. */
private readonly plugins: Map<
PluginName,

View file

@ -84,6 +84,7 @@ export async function bootstrap({
try {
await root.setup();
await root.start();
} catch (err) {
await shutdown(err);
}

View file

@ -39,6 +39,7 @@ type ElasticsearchServiceContract = PublicMethodsOf<ElasticsearchService>;
const createMock = () => {
const mocked: jest.Mocked<ElasticsearchServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockResolvedValue(createSetupContractMock());

View file

@ -106,6 +106,8 @@ export class ElasticsearchService implements CoreService<ElasticsearchServiceSet
};
}
public async start() {}
public async stop() {
this.log.debug('Stopping elasticsearch service');

View file

@ -1,49 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`logs error if already set up 1`] = `
Object {
"debug": Array [],
"error": Array [],
"fatal": Array [],
"info": Array [],
"log": Array [],
"trace": Array [],
"warn": Array [
Array [
"Received new HTTP config after server was started. Config will **not** be applied.",
],
Array [
Array [
"Received new HTTP config after server was started. Config will **not** be applied.",
],
}
`;
exports[`register route handler 1`] = `
Object {
"debug": Array [
Array [
"registering route handler for [/foo]",
],
],
"error": Array [],
"fatal": Array [],
"info": Array [],
"log": Array [],
"trace": Array [],
"warn": Array [],
}
`;
exports[`throws if registering route handler after http server is set up 1`] = `
Object {
"debug": Array [],
"error": Array [
Array [
"Received new router [/foo] after server was started. Router will **not** be applied.",
],
],
"fatal": Array [],
"info": Array [],
"log": Array [],
"trace": Array [],
"warn": Array [],
}
]
`;

View file

@ -83,7 +83,7 @@ const createHttpSchema = schema.object(
}
);
type HttpConfigType = TypeOf<typeof createHttpSchema>;
export type HttpConfigType = TypeOf<typeof createHttpSchema>;
export class HttpConfig {
/**

View file

@ -56,6 +56,7 @@ afterEach(async () => {
test('listening after started', async () => {
expect(server.isListening()).toBe(false);
await server.setup(config);
await server.start(config);
expect(server.isListening()).toBe(true);
@ -68,9 +69,9 @@ test('200 OK with body', async () => {
return res.ok({ key: 'value' });
});
server.registerRouter(router);
const { server: innerServer } = await server.start(config);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await supertest(innerServer.listener)
.get('/foo/')
@ -87,9 +88,10 @@ test('202 Accepted with body', async () => {
return res.accepted({ location: 'somewhere' });
});
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.get('/foo/')
@ -106,9 +108,10 @@ test('204 No content', async () => {
return res.noContent();
});
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.get('/foo/')
@ -127,9 +130,10 @@ test('400 Bad request with error', async () => {
return res.badRequest(err);
});
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.get('/foo/')
@ -156,9 +160,10 @@ test('valid params', async () => {
}
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.get('/foo/some-string')
@ -185,9 +190,10 @@ test('invalid params', async () => {
}
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.get('/foo/some-string')
@ -217,9 +223,10 @@ test('valid query', async () => {
}
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.get('/foo/?bar=test&quux=123')
@ -246,9 +253,10 @@ test('invalid query', async () => {
}
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.get('/foo/?bar=test')
@ -278,9 +286,10 @@ test('valid body', async () => {
}
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.post('/foo/')
@ -311,9 +320,10 @@ test('invalid body', async () => {
}
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.post('/foo/')
@ -343,9 +353,10 @@ test('handles putting', async () => {
}
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.put('/foo/')
@ -373,9 +384,10 @@ test('handles deleting', async () => {
}
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.delete('/foo/3')
@ -398,9 +410,10 @@ test('filtered headers', async () => {
return res.noContent();
});
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(config);
await server.start(config);
await supertest(innerServer.listener)
.get('/foo/?bar=quux')
@ -430,9 +443,10 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => {
res.ok({ key: 'value:/foo' })
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(configWithBasePath);
await server.start(configWithBasePath);
innerServerListener = innerServer.listener;
});
@ -490,9 +504,10 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => {
res.ok({ key: 'value:/foo' })
);
server.registerRouter(router);
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
const { server: innerServer } = await server.start(configWithBasePath);
await server.start(configWithBasePath);
innerServerListener = innerServer.listener;
});
@ -555,17 +570,19 @@ describe('with defined `redirectHttpFromPort`', () => {
const router = new Router('/');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ key: 'value:/' }));
server.registerRouter(router);
const { registerRouter } = await server.setup(config);
registerRouter(router);
await server.start(configWithSSL);
});
});
test('returns server and connection options on start', async () => {
const { server: innerServer, options } = await server.start({
const configWithPort = {
...config,
port: 12345,
});
};
const { options, server: innerServer } = await server.setup(configWithPort);
expect(innerServer).toBeDefined();
expect(innerServer).toBe((server as any).server);
@ -573,7 +590,7 @@ test('returns server and connection options on start', async () => {
});
test('registers auth request interceptor only once', async () => {
const { registerAuth } = await server.start(config);
const { registerAuth } = await server.setup(config);
const doRegister = () =>
registerAuth(() => null as any, {
encryptionKey: 'any_password',
@ -584,9 +601,15 @@ test('registers auth request interceptor only once', async () => {
});
test('registers onRequest interceptor several times', async () => {
const { registerOnRequest } = await server.start(config);
const { registerOnRequest } = await server.setup(config);
const doRegister = () => registerOnRequest(() => null as any);
doRegister();
expect(doRegister).not.toThrowError();
});
test('throws an error if starts without set up', async () => {
await expect(server.start(config)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Http server is not setup up yet"`
);
});

View file

@ -26,14 +26,17 @@ import { createServer, getServerOptions } from './http_tools';
import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
import { adoptToHapiOnRequestFormat, OnRequestHandler } from './lifecycle/on_request';
import { Router } from './router';
import { deepFreeze, RecursiveReadonly } from './lib/deep_freeze';
import {
SessionStorageCookieOptions,
createCookieSessionStorageFactory,
} from './cookie_session_storage';
export interface HttpServerInfo {
export interface HttpServerSetup {
server: Server;
options: ServerOptions;
options: RecursiveReadonly<ServerOptions>;
registerRouter: (router: Router) => void;
/**
* Define custom authentication and/or authorization mechanism for incoming requests.
* Applied to all resources by default. Only one AuthenticationHandler can be registered.
@ -61,20 +64,40 @@ export class HttpServer {
return this.server !== undefined && this.server.listener.listening;
}
public registerRouter(router: Router) {
private registerRouter(router: Router) {
if (this.isListening()) {
throw new Error('Routers can be registered only when HTTP server is stopped.');
}
this.log.debug(`registering route handler for [${router.path}]`);
this.registeredRouters.add(router);
}
public async start(config: HttpConfig): Promise<HttpServerInfo> {
this.log.debug('starting http server');
public setup(config: HttpConfig): HttpServerSetup {
const serverOptions = getServerOptions(config);
this.server = createServer(serverOptions);
return {
options: deepFreeze(serverOptions),
registerRouter: this.registerRouter.bind(this),
registerOnRequest: this.registerOnRequest.bind(this),
registerAuth: <T>(
fn: AuthenticationHandler<T>,
cookieOptions: SessionStorageCookieOptions<T>
) => this.registerAuth(fn, cookieOptions, config.basePath),
// Return server instance with the connection options so that we can properly
// bridge core and the "legacy" Kibana internally. Once this bridge isn't
// needed anymore we shouldn't return the instance from this method.
server: this.server,
};
}
public async start(config: HttpConfig) {
if (this.server === undefined) {
throw new Error('Http server is not setup up yet');
}
this.log.debug('starting http server');
this.setupBasePathRewrite(this.server, config);
for (const router of this.registeredRouters) {
@ -94,19 +117,6 @@ export class HttpServer {
config.rewriteBasePath ? config.basePath : ''
}`
);
// Return server instance with the connection options so that we can properly
// bridge core and the "legacy" Kibana internally. Once this bridge isn't
// needed anymore we shouldn't return anything from this method.
return {
server: this.server,
options: serverOptions,
registerOnRequest: this.registerOnRequest.bind(this),
registerAuth: <T>(
fn: AuthenticationHandler<T>,
cookieOptions: SessionStorageCookieOptions<T>
) => this.registerAuth(fn, cookieOptions, config.basePath),
};
}
public async stop() {

View file

@ -22,23 +22,33 @@ import { HttpService } from './http_service';
const createSetupContractMock = () => {
const setupContract = {
// we can mock some hapi server method when we need it
server: {} as Server,
options: {} as ServerOptions,
registerAuth: jest.fn(),
registerOnRequest: jest.fn(),
registerRouter: jest.fn(),
// we can mock some hapi server method when we need it
server: {} as Server,
};
return setupContract;
};
const createStartContractMock = () => {
const startContract = {
isListening: jest.fn(),
};
startContract.isListening.mockReturnValue(true);
return startContract;
};
type HttpServiceContract = PublicMethodsOf<HttpService>;
const createHttpServiceMock = () => {
const mocked: jest.Mocked<HttpServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
registerRouter: jest.fn(),
};
mocked.setup.mockResolvedValue(createSetupContractMock());
mocked.start.mockResolvedValue(createStartContractMock());
return mocked;
};

View file

@ -21,75 +21,91 @@ import { mockHttpServer } from './http_service.test.mocks';
import { noop } from 'lodash';
import { BehaviorSubject } from 'rxjs';
import { HttpConfig, HttpService, Router } from '.';
import { HttpService, Router } from '.';
import { HttpConfigType } from './http_config';
import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
const logger = loggingServiceMock.create();
const env = Env.createDefault(getEnvOptions());
const createConfigService = (value: Partial<HttpConfigType> = {}) =>
new ConfigService(
new BehaviorSubject<Config>(
new ObjectToConfigAdapter({
http: value,
})
),
env,
logger
);
afterEach(() => {
jest.clearAllMocks();
});
test('creates and sets up http server', async () => {
const config = {
const configService = createConfigService({
host: 'example.org',
port: 1234,
ssl: {},
} as HttpConfig;
const config$ = new BehaviorSubject(config);
});
const httpServer = {
isListening: () => false,
setup: jest.fn(),
start: jest.fn(),
stop: noop,
};
mockHttpServer.mockImplementation(() => httpServer);
const service = new HttpService(config$.asObservable(), logger);
const service = new HttpService({ configService, env, logger });
expect(mockHttpServer.mock.instances.length).toBe(1);
expect(httpServer.start).not.toHaveBeenCalled();
expect(httpServer.setup).not.toHaveBeenCalled();
await service.setup();
expect(httpServer.setup).toHaveBeenCalledTimes(1);
expect(httpServer.start).not.toHaveBeenCalled();
await service.start();
expect(httpServer.start).toHaveBeenCalledTimes(1);
});
test('logs error if already set up', async () => {
const config = { ssl: {} } as HttpConfig;
const config$ = new BehaviorSubject(config);
const configService = createConfigService();
const httpServer = {
isListening: () => true,
setup: jest.fn(),
start: noop,
stop: noop,
};
mockHttpServer.mockImplementation(() => httpServer);
const service = new HttpService(config$.asObservable(), logger);
const service = new HttpService({ configService, env, logger });
await service.setup();
expect(loggingServiceMock.collect(logger)).toMatchSnapshot();
expect(loggingServiceMock.collect(logger).warn).toMatchSnapshot();
});
test('stops http server', async () => {
const config = { ssl: {} } as HttpConfig;
const config$ = new BehaviorSubject(config);
const configService = createConfigService();
const httpServer = {
isListening: () => false,
setup: noop,
start: noop,
stop: jest.fn(),
};
mockHttpServer.mockImplementation(() => httpServer);
const service = new HttpService(config$.asObservable(), logger);
const service = new HttpService({ configService, env, logger });
await service.setup();
await service.start();
expect(httpServer.stop).toHaveBeenCalledTimes(0);
@ -98,52 +114,30 @@ test('stops http server', async () => {
expect(httpServer.stop).toHaveBeenCalledTimes(1);
});
test('register route handler', () => {
const config = {} as HttpConfig;
const config$ = new BehaviorSubject(config);
test('register route handler', async () => {
const configService = createConfigService({});
const registerRouterMock = jest.fn();
const httpServer = {
isListening: () => false,
registerRouter: jest.fn(),
setup: () => ({ registerRouter: registerRouterMock }),
start: noop,
stop: noop,
};
mockHttpServer.mockImplementation(() => httpServer);
const service = new HttpService(config$.asObservable(), logger);
const service = new HttpService({ configService, env, logger });
const router = new Router('/foo');
service.registerRouter(router);
const { registerRouter } = await service.setup();
registerRouter(router);
expect(httpServer.registerRouter).toHaveBeenCalledTimes(1);
expect(httpServer.registerRouter).toHaveBeenLastCalledWith(router);
expect(loggingServiceMock.collect(logger)).toMatchSnapshot();
});
test('throws if registering route handler after http server is set up', () => {
const config = {} as HttpConfig;
const config$ = new BehaviorSubject(config);
const httpServer = {
isListening: () => true,
registerRouter: jest.fn(),
start: noop,
stop: noop,
};
mockHttpServer.mockImplementation(() => httpServer);
const service = new HttpService(config$.asObservable(), logger);
const router = new Router('/foo');
service.registerRouter(router);
expect(httpServer.registerRouter).toHaveBeenCalledTimes(0);
expect(loggingServiceMock.collect(logger)).toMatchSnapshot();
expect(registerRouterMock).toHaveBeenCalledTimes(1);
expect(registerRouterMock).toHaveBeenLastCalledWith(router);
});
test('returns http server contract on setup', async () => {
const configService = createConfigService();
const httpServer = {
server: {},
options: { someOption: true },
@ -151,11 +145,57 @@ test('returns http server contract on setup', async () => {
mockHttpServer.mockImplementation(() => ({
isListening: () => false,
start: jest.fn().mockReturnValue(httpServer),
setup: jest.fn().mockReturnValue(httpServer),
stop: noop,
}));
const service = new HttpService(new BehaviorSubject({ ssl: {} } as HttpConfig), logger);
const service = new HttpService({ configService, env, logger });
expect(await service.setup()).toBe(httpServer);
});
test('does not start http server if process is dev cluster master', async () => {
const configService = createConfigService({});
const httpServer = {
isListening: () => false,
setup: noop,
start: jest.fn(),
stop: noop,
};
mockHttpServer.mockImplementation(() => httpServer);
const service = new HttpService({
configService,
env: new Env('.', getEnvOptions({ isDevClusterMaster: true })),
logger,
});
await service.setup();
await service.start();
expect(httpServer.start).not.toHaveBeenCalled();
});
test('does not start http server if configured with `autoListen:false`', async () => {
const configService = createConfigService({
autoListen: false,
});
const httpServer = {
isListening: () => false,
setup: noop,
start: jest.fn(),
stop: noop,
};
mockHttpServer.mockImplementation(() => httpServer);
const service = new HttpService({
configService,
env: new Env('.', getEnvOptions({ isDevClusterMaster: true })),
logger,
});
await service.setup();
await service.start();
expect(httpServer.start).not.toHaveBeenCalled();
});

View file

@ -21,28 +21,37 @@ import { Observable, Subscription } from 'rxjs';
import { first } from 'rxjs/operators';
import { CoreService } from '../../types';
import { Logger, LoggerFactory } from '../logging';
import { Logger } from '../logging';
import { CoreContext } from '../core_context';
import { HttpConfig } from './http_config';
import { HttpServer, HttpServerInfo } from './http_server';
import { HttpServer, HttpServerSetup } from './http_server';
import { HttpsRedirectServer } from './https_redirect_server';
import { Router } from './router';
/** @public */
export type HttpServiceSetup = HttpServerInfo;
export type HttpServiceSetup = HttpServerSetup;
/** @public */
export interface HttpServiceStart {
/** Indicates if http server is listening on a port */
isListening: () => boolean;
}
/** @internal */
export class HttpService implements CoreService<HttpServiceSetup> {
export class HttpService implements CoreService<HttpServiceSetup, HttpServiceStart> {
private readonly httpServer: HttpServer;
private readonly httpsRedirectServer: HttpsRedirectServer;
private readonly config$: Observable<HttpConfig>;
private configSubscription?: Subscription;
private readonly log: Logger;
constructor(private readonly config$: Observable<HttpConfig>, logger: LoggerFactory) {
this.log = logger.get('http');
constructor(private readonly coreContext: CoreContext) {
this.log = coreContext.logger.get('http');
this.config$ = this.coreContext.configService.atPath('server', HttpConfig);
this.httpServer = new HttpServer(logger.get('http', 'server'));
this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server'));
this.httpServer = new HttpServer(coreContext.logger.get('http', 'server'));
this.httpsRedirectServer = new HttpsRedirectServer(
coreContext.logger.get('http', 'redirect', 'server')
);
}
public async setup() {
@ -58,16 +67,29 @@ export class HttpService implements CoreService<HttpServiceSetup> {
const config = await this.config$.pipe(first()).toPromise();
// If a redirect port is specified, we start an HTTP server at this port and
// redirect all requests to the SSL port.
if (config.ssl.enabled && config.ssl.redirectHttpFromPort !== undefined) {
await this.httpsRedirectServer.start(config);
return this.httpServer.setup(config);
}
public async start() {
const config = await this.config$.pipe(first()).toPromise();
// We shouldn't set up http service in two cases:`
// 1. If `server.autoListen` is explicitly set to `false`.
// 2. When the process is run as dev cluster master in which case cluster manager
// will fork a dedicated process where http service will be set up instead.
if (!this.coreContext.env.isDevClusterMaster && config.autoListen) {
// If a redirect port is specified, we start an HTTP server at this port and
// redirect all requests to the SSL port.
if (config.ssl.enabled && config.ssl.redirectHttpFromPort !== undefined) {
await this.httpsRedirectServer.start(config);
}
await this.httpServer.start(config);
}
// The HttpService's setup method calls `start` on HttpServer because it is
// a more appropriate name. In the future, starting the server should be moved
// to the `start` lifecycle handler of HttpService.
return await this.httpServer.start(config);
return {
isListening: () => this.httpServer.isListening(),
};
}
public async stop() {
@ -81,19 +103,4 @@ export class HttpService implements CoreService<HttpServiceSetup> {
await this.httpServer.stop();
await this.httpsRedirectServer.stop();
}
public registerRouter(router: Router): void {
if (this.httpServer.isListening()) {
// If the server is already running we can't make any config changes
// to it, so we warn and don't allow the config to pass through.
// TODO Should we throw instead?
this.log.error(
`Received new router [${router.path}] after server was started. ` +
'Router will **not** be applied.'
);
} else {
this.log.debug(`registering route handler for [${router.path}]`);
this.httpServer.registerRouter(router);
}
}
}

View file

@ -18,9 +18,8 @@
*/
export { HttpConfig } from './http_config';
export { HttpService, HttpServiceSetup } from './http_service';
export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service';
export { Router, KibanaRequest } from './router';
export { HttpServerInfo } from './http_server';
export { BasePathProxyServer } from './base_path_proxy_server';
export { AuthenticationHandler, AuthToolkit } from './lifecycle/auth';
export { OnRequestHandler, OnRequestToolkit } from './lifecycle/on_request';

View file

@ -43,10 +43,10 @@ describe('http service', () => {
router.get({ path: authUrl.auth, validate: false }, async (req, res) =>
res.ok({ content: 'ok' })
);
// TODO fix me when registerRouter is available before HTTP server is run
(root as any).server.http.registerRouter(router);
await root.setup();
const { http } = await root.setup();
http.registerRouter(router);
await root.start();
}, 30000);
afterAll(async () => await root.shutdown());
@ -129,10 +129,11 @@ describe('http service', () => {
[onReqUrl.root, onReqUrl.independentReq].forEach(url =>
router.get({ path: url, validate: false }, async (req, res) => res.ok({ content: 'ok' }))
);
// TODO fix me when registerRouter is available before HTTP server is run
(root as any).server.http.registerRouter(router);
await root.setup();
const { http } = await root.setup();
http.registerRouter(router);
await root.start();
}, 30000);
afterAll(async () => await root.shutdown());

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.
*/
type Freezable = { [k: string]: any } | any[];
// if we define this inside RecursiveReadonly TypeScript complains
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RecursiveReadonlyArray<T> extends Array<RecursiveReadonly<T>> {}
export type RecursiveReadonly<T> = T extends any[]
? RecursiveReadonlyArray<T[number]>
: T extends object
? Readonly<{ [K in keyof T]: RecursiveReadonly<T[K]> }>
: T;
export function deepFreeze<T extends Freezable>(object: T) {
// for any properties that reference an object, makes sure that object is
// recursively frozen as well
for (const value of Object.values(object)) {
if (value !== null && typeof value === 'object') {
deepFreeze(value);
}
}
return Object.freeze(object) as RecursiveReadonly<T>;
}

View file

@ -23,7 +23,7 @@ jest.doMock('./http/http_service', () => ({
HttpService: jest.fn(() => httpService),
}));
export const mockPluginsService = { setup: jest.fn(), stop: jest.fn() };
export const mockPluginsService = { setup: jest.fn(), start: jest.fn(), stop: jest.fn() };
jest.doMock('./plugins/plugins_service', () => ({
PluginsService: jest.fn(() => mockPluginsService),
}));
@ -34,7 +34,7 @@ jest.doMock('./elasticsearch/elasticsearch_service', () => ({
ElasticsearchService: jest.fn(() => elasticsearchService),
}));
export const mockLegacyService = { setup: jest.fn(), stop: jest.fn() };
export const mockLegacyService = { setup: jest.fn(), start: jest.fn(), stop: jest.fn() };
jest.mock('./legacy/legacy_service', () => ({
LegacyService: jest.fn(() => mockLegacyService),
}));

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { ElasticsearchServiceSetup } from './elasticsearch';
import { HttpServiceSetup } from './http';
import { HttpServiceSetup, HttpServiceStart } from './http';
import { PluginsServiceSetup } from './plugins';
export { bootstrap } from './bootstrap';
@ -56,4 +56,8 @@ export interface CoreSetup {
plugins: PluginsServiceSetup;
}
export { HttpServiceSetup, ElasticsearchServiceSetup, PluginsServiceSetup };
export interface CoreStart {
http: HttpServiceStart;
}
export { HttpServiceSetup, HttpServiceStart, ElasticsearchServiceSetup, PluginsServiceSetup };

View file

@ -36,7 +36,7 @@ Array [
"debug": [MockFunction] {
"calls": Array [
Array [
"setting up legacy service",
"starting legacy service",
],
],
"results": Array [

View file

@ -32,6 +32,7 @@ import { Config, Env, ObjectToConfigAdapter } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { configServiceMock } from '../config/config_service.mock';
import { ElasticsearchServiceSetup } from '../elasticsearch';
import { HttpServiceStart } from '../http';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins';
import { PluginsServiceSetup } from '../plugins/plugins_service';
@ -48,6 +49,11 @@ let setupDeps: {
http: any;
plugins: PluginsServiceSetup;
};
let startDeps: {
http: HttpServiceStart;
};
const logger = loggingServiceMock.create();
let configService: ReturnType<typeof configServiceMock.create>;
@ -60,8 +66,8 @@ beforeEach(() => {
setupDeps = {
elasticsearch: { legacy: {} } as any,
http: {
server: { listener: { addListener: jest.fn() }, route: jest.fn() },
options: { someOption: 'foo', someAnotherOption: 'bar' },
server: { listener: { addListener: jest.fn() }, route: jest.fn() },
},
plugins: {
contracts: new Map([['plugin-id', 'plugin-value']]),
@ -72,6 +78,12 @@ beforeEach(() => {
},
};
startDeps = {
http: {
isListening: () => true,
},
};
config$ = new BehaviorSubject<Config>(
new ObjectToConfigAdapter({
elasticsearch: { hosts: ['http://127.0.0.1'] },
@ -92,6 +104,7 @@ afterEach(() => {
describe('once LegacyService is set up with connection info', () => {
test('register proxy route.', async () => {
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
expect(setupDeps.http.server.route.mock.calls).toMatchSnapshot('proxy route options');
});
@ -107,7 +120,8 @@ describe('once LegacyService is set up with connection info', () => {
// Wait until listen is called and proxy route is registered, but don't allow
// listen to complete and make kbnServer available.
const legacySetupPromise = legacyService.setup(setupDeps);
await legacyService.setup(setupDeps);
const legacySetupPromise = legacyService.start(startDeps);
await kbnServerListen$.pipe(first()).toPromise();
const mockResponse: any = {
@ -156,20 +170,20 @@ describe('once LegacyService is set up with connection info', () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
expect(MockKbnServer).toHaveBeenCalledTimes(1);
expect(MockKbnServer).toHaveBeenCalledWith(
{ server: { autoListen: true } },
{
elasticsearch: setupDeps.elasticsearch,
http: setupDeps.http,
setupDeps,
startDeps,
serverOptions: {
listener: expect.any(LegacyPlatformProxy),
someAnotherOption: 'bar',
someOption: 'foo',
},
handledConfigPaths: ['foo.bar'],
plugins: setupDeps.plugins,
}
);
@ -182,20 +196,20 @@ describe('once LegacyService is set up with connection info', () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false }));
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
expect(MockKbnServer).toHaveBeenCalledTimes(1);
expect(MockKbnServer).toHaveBeenCalledWith(
{ server: { autoListen: true } },
{
elasticsearch: setupDeps.elasticsearch,
http: setupDeps.http,
setupDeps,
startDeps,
serverOptions: {
listener: expect.any(LegacyPlatformProxy),
someAnotherOption: 'bar',
someOption: 'foo',
},
handledConfigPaths: ['foo.bar'],
plugins: setupDeps.plugins,
}
);
@ -209,7 +223,8 @@ describe('once LegacyService is set up with connection info', () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed'));
await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingSnapshot();
await legacyService.setup(setupDeps);
await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot();
const [mockKbnServer] = MockKbnServer.mock.instances;
expect(mockKbnServer.listen).toHaveBeenCalled();
@ -219,7 +234,8 @@ 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')));
await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingSnapshot();
await legacyService.setup(setupDeps);
await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot();
expect(MockKbnServer).not.toHaveBeenCalled();
expect(MockClusterManager).not.toHaveBeenCalled();
@ -227,6 +243,7 @@ describe('once LegacyService is set up with connection info', () => {
test('reconfigures logging configuration if new config is received.', async () => {
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
const [mockKbnServer] = MockKbnServer.mock.instances as Array<jest.Mocked<KbnServer>>;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
@ -240,6 +257,7 @@ describe('once LegacyService is set up with connection info', () => {
test('logs error if re-configuring fails.', async () => {
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
const [mockKbnServer] = MockKbnServer.mock.instances as Array<jest.Mocked<KbnServer>>;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
@ -257,6 +275,7 @@ describe('once LegacyService is set up with connection info', () => {
test('logs error if config service fails.', async () => {
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
const [mockKbnServer] = MockKbnServer.mock.instances;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
@ -274,6 +293,7 @@ describe('once LegacyService is set up with connection info', () => {
const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } };
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
const [[{ handler }]] = setupDeps.http.server.route.mock.calls;
const response = await handler(mockRequest, mockResponseToolkit);
@ -293,11 +313,14 @@ describe('once LegacyService is set up with connection info', () => {
});
describe('once LegacyService is set up without connection info', () => {
const disabledHttpStartDeps = {
http: {
isListening: () => false,
},
};
beforeEach(async () => {
await legacyService.setup({
elasticsearch: setupDeps.elasticsearch,
plugins: setupDeps.plugins,
});
await legacyService.setup(setupDeps);
await legacyService.start(disabledHttpStartDeps);
});
test('creates legacy kbnServer with `autoListen: false`.', () => {
@ -306,10 +329,10 @@ describe('once LegacyService is set up without connection info', () => {
expect(MockKbnServer).toHaveBeenCalledWith(
{ server: { autoListen: true } },
{
elasticsearch: setupDeps.elasticsearch,
setupDeps,
startDeps: disabledHttpStartDeps,
serverOptions: { autoListen: false },
handledConfigPaths: ['foo.bar'],
plugins: setupDeps.plugins,
}
);
});
@ -350,6 +373,12 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
await devClusterLegacyService.setup({
elasticsearch: setupDeps.elasticsearch,
plugins: { contracts: new Map(), uiPlugins: { public: new Map(), internal: new Map() } },
http: setupDeps.http,
});
await devClusterLegacyService.start({
http: {
isListening: () => false,
},
});
expect(MockClusterManager.create.mock.calls).toMatchSnapshot(
@ -372,6 +401,12 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
await devClusterLegacyService.setup({
elasticsearch: setupDeps.elasticsearch,
plugins: { contracts: new Map(), uiPlugins: { public: new Map(), internal: new Map() } },
http: setupDeps.http,
});
await devClusterLegacyService.start({
http: {
isListening: () => false,
},
});
expect(MockClusterManager.create.mock.calls).toMatchSnapshot(
@ -379,3 +414,9 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
);
});
});
test('Cannot start without setup phase', async () => {
await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Legacy service is not setup yet."`
);
});

View file

@ -21,13 +21,12 @@ import { Server as HapiServer } from 'hapi';
import { combineLatest, ConnectableObservable, EMPTY, Subscription } from 'rxjs';
import { first, map, mergeMap, publishReplay, tap } from 'rxjs/operators';
import { CoreService } from '../../types';
import { CoreSetup, CoreStart } from '../../server';
import { Config } from '../config';
import { CoreContext } from '../core_context';
import { DevConfig } from '../dev';
import { ElasticsearchServiceSetup } from '../elasticsearch';
import { BasePathProxyServer, HttpConfig, HttpServiceSetup } from '../http';
import { BasePathProxyServer, HttpConfig } from '../http';
import { Logger } from '../logging';
import { PluginsServiceSetup } from '../plugins/plugins_service';
import { LegacyPlatformProxy } from './legacy_platform_proxy';
interface LegacyKbnServer {
@ -37,12 +36,6 @@ interface LegacyKbnServer {
close: () => Promise<void>;
}
interface SetupDeps {
elasticsearch: ElasticsearchServiceSetup;
http?: HttpServiceSetup;
plugins: PluginsServiceSetup;
}
function getLegacyRawConfig(config: Config) {
const rawConfig = config.toRaw();
@ -60,13 +53,20 @@ export class LegacyService implements CoreService {
private readonly log: Logger;
private kbnServer?: LegacyKbnServer;
private configSubscription?: Subscription;
private setupDeps?: CoreSetup;
constructor(private readonly coreContext: CoreContext) {
this.log = coreContext.logger.get('legacy-service');
}
public async setup(deps: SetupDeps) {
this.log.debug('setting up legacy service');
public async setup(setupDeps: CoreSetup) {
this.setupDeps = setupDeps;
}
public async start(startDeps: CoreStart) {
const { setupDeps } = this;
if (!setupDeps) {
throw new Error('Legacy service is not setup yet.');
}
this.log.debug('starting legacy service');
const update$ = this.coreContext.configService.getConfig$().pipe(
tap(config => {
@ -89,7 +89,7 @@ export class LegacyService implements CoreService {
await this.createClusterManager(config);
return;
}
return await this.createKbnServer(config, deps);
return await this.createKbnServer(config, setupDeps, startDeps);
})
)
.toPromise();
@ -130,7 +130,7 @@ export class LegacyService implements CoreService {
);
}
private async createKbnServer(config: Config, { elasticsearch, http, plugins }: SetupDeps) {
private async createKbnServer(config: Config, setupDeps: CoreSetup, startDeps: CoreStart) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const KbnServer = require('../../../legacy/server/kbn_server');
const kbnServer: LegacyKbnServer = new KbnServer(getLegacyRawConfig(config), {
@ -139,17 +139,15 @@ export class LegacyService implements CoreService {
// bridge with the "legacy" Kibana. If server isn't run (e.g. if process is
// managed by ClusterManager or optimizer) then we won't have that info,
// so we can't start "legacy" server either.
serverOptions:
http !== undefined
? {
...http.options,
listener: this.setupProxyListener(http.server),
}
: { autoListen: false },
serverOptions: startDeps.http.isListening()
? {
...setupDeps.http.options,
listener: this.setupProxyListener(setupDeps.http.server),
}
: { autoListen: false },
handledConfigPaths: await this.coreContext.configService.getUsedPaths(),
http,
elasticsearch,
plugins,
setupDeps,
startDeps,
});
// The kbnWorkerType check is necessary to prevent the repl

View file

@ -23,6 +23,7 @@ import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { PluginWrapper, PluginManifest } from './plugin';
@ -59,7 +60,10 @@ function createPluginManifest(manifestProps: Partial<PluginManifest> = {}): Plug
let configService: ConfigService;
let env: Env;
let coreContext: CoreContext;
const setupDeps = { elasticsearch: elasticsearchServiceMock.createSetupContract() };
const setupDeps = {
elasticsearch: elasticsearchServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
};
beforeEach(() => {
env = Env.createDefault(getEnvOptions());

View file

@ -114,12 +114,6 @@ export function createPluginInitializerContext(
};
}
// Added to improve http typings as make { http: Required<HttpSetup> }
// Http service is disabled, when Kibana runs in optimizer mode or as dev cluster managed by cluster master.
// In theory no plugins shouldn try to access http dependency in this case.
function preventAccess() {
throw new Error('Cannot use http contract when http server not started');
}
/**
* This returns a facade for `CoreContext` that will be exposed to the plugin `setup` method.
* This facade should be safe to use only within `setup` itself.
@ -144,14 +138,9 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
adminClient$: deps.elasticsearch.adminClient$,
dataClient$: deps.elasticsearch.dataClient$,
},
http: deps.http
? {
registerAuth: deps.http.registerAuth,
registerOnRequest: deps.http.registerOnRequest,
}
: {
registerAuth: preventAccess,
registerOnRequest: preventAccess,
},
http: {
registerAuth: deps.http.registerAuth,
registerOnRequest: deps.http.registerOnRequest,
},
};
}

View file

@ -25,6 +25,7 @@ import { BehaviorSubject, from } from 'rxjs';
import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { PluginDiscoveryError } from './discovery';
import { PluginWrapper } from './plugin';
@ -37,7 +38,10 @@ let pluginsService: PluginsService;
let configService: ConfigService;
let env: Env;
let mockPluginSystem: jest.Mocked<PluginsSystem>;
const setupDeps = { elasticsearch: elasticsearchServiceMock.createSetupContract() };
const setupDeps = {
elasticsearch: elasticsearchServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
};
const logger = loggingServiceMock.create();
beforeEach(() => {
mockPackage.raw = {

View file

@ -41,7 +41,7 @@ export interface PluginsServiceSetup {
/** @internal */
export interface PluginsServiceSetupDeps {
elasticsearch: ElasticsearchServiceSetup;
http?: HttpServiceSetup;
http: HttpServiceSetup;
}
/** @internal */
@ -80,6 +80,8 @@ export class PluginsService implements CoreService<PluginsServiceSetup> {
};
}
public async start() {}
public async stop() {
this.log.debug('Stopping plugins service');
await this.pluginsSystem.stopPlugins();

View file

@ -24,6 +24,7 @@ import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
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 { PluginsSystem } from './plugins_system';
@ -57,7 +58,10 @@ let pluginsSystem: PluginsSystem;
let configService: ConfigService;
let env: Env;
let coreContext: CoreContext;
const setupDeps = { elasticsearch: elasticsearchServiceMock.createSetupContract() };
const setupDeps = {
elasticsearch: elasticsearchServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
};
beforeEach(() => {
env = Env.createDefault(getEnvOptions());

View file

@ -53,7 +53,18 @@ export class Root {
try {
await this.setupLogging();
await this.server.setup();
return await this.server.setup();
} catch (e) {
await this.shutdown(e);
throw e;
}
}
public async start() {
this.log.debug('starting root');
try {
return await this.server.start();
} catch (e) {
await this.shutdown(e);
throw e;

View file

@ -74,6 +74,12 @@ export interface CoreSetup {
plugins: PluginsServiceSetup;
}
// @public (undocumented)
export interface CoreStart {
// (undocumented)
http: HttpServiceStart;
}
// @internal
export interface DiscoveredPlugin {
readonly configPath: ConfigPath;
@ -108,7 +114,12 @@ export interface ElasticsearchServiceSetup {
export type Headers = Record<string, string | string[] | undefined>;
// @public (undocumented)
export type HttpServiceSetup = HttpServerInfo;
export type HttpServiceSetup = HttpServerSetup;
// @public (undocumented)
export interface HttpServiceStart {
isListening: () => boolean;
}
// @public (undocumented)
export class KibanaRequest<Params, Query, Body> {

View file

@ -44,13 +44,15 @@ afterEach(() => {
jest.clearAllMocks();
configService.atPath.mockReset();
httpService.setup.mockReset();
httpService.setup.mockClear();
httpService.start.mockClear();
httpService.stop.mockReset();
elasticsearchService.setup.mockReset();
elasticsearchService.stop.mockReset();
mockPluginsService.setup.mockReset();
mockPluginsService.stop.mockReset();
mockLegacyService.setup.mockReset();
mockLegacyService.start.mockReset();
mockLegacyService.stop.mockReset();
});
@ -63,54 +65,39 @@ test('sets up services on "setup"', async () => {
expect(httpService.setup).not.toHaveBeenCalled();
expect(elasticsearchService.setup).not.toHaveBeenCalled();
expect(mockPluginsService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.start).not.toHaveBeenCalled();
await server.setup();
expect(httpService.setup).toHaveBeenCalledTimes(1);
expect(elasticsearchService.setup).toHaveBeenCalledTimes(1);
expect(mockPluginsService.setup).toHaveBeenCalledTimes(1);
expect(mockLegacyService.setup).toHaveBeenCalledTimes(1);
});
test('runs services on "start"', async () => {
const mockPluginsServiceSetup = new Map([['some-plugin', 'some-value']]);
mockPluginsService.setup.mockReturnValue(Promise.resolve(mockPluginsServiceSetup));
const server = new Server(configService as any, logger, env);
expect(httpService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.start).not.toHaveBeenCalled();
await server.setup();
await server.start();
expect(httpService.start).toHaveBeenCalledTimes(1);
expect(mockLegacyService.start).toHaveBeenCalledTimes(1);
});
test('does not fail on "setup" if there are unused paths detected', async () => {
configService.getUnusedPaths.mockResolvedValue(['some.path', 'another.path']);
const server = new Server(configService as any, logger, env);
await expect(server.setup()).resolves.toBeUndefined();
await expect(server.setup()).resolves.toBeDefined();
expect(loggingServiceMock.collect(logger)).toMatchSnapshot('unused paths logs');
});
test('does not setup http service is `autoListen:false`', async () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false }));
const server = new Server(configService as any, logger, env);
expect(mockLegacyService.setup).not.toHaveBeenCalled();
await server.setup();
expect(httpService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.setup).toHaveBeenCalledTimes(1);
expect(mockLegacyService.setup).toHaveBeenCalledWith({});
});
test('does not setup http service if process is dev cluster master', async () => {
const server = new Server(
configService as any,
logger,
new Env('.', getEnvOptions({ isDevClusterMaster: true }))
);
expect(mockLegacyService.setup).not.toHaveBeenCalled();
await server.setup();
expect(httpService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.setup).toHaveBeenCalledTimes(1);
expect(mockLegacyService.setup).toHaveBeenCalledWith({});
});
test('stops services on "stop"', async () => {
const server = new Server(configService as any, logger, env);

View file

@ -17,10 +17,9 @@
* under the License.
*/
import { first } from 'rxjs/operators';
import { ConfigService, Env } from './config';
import { ElasticsearchService } from './elasticsearch';
import { HttpConfig, HttpService, HttpServiceSetup, Router } from './http';
import { HttpService, HttpServiceSetup, Router } from './http';
import { LegacyService } from './legacy';
import { Logger, LoggerFactory } from './logging';
import { PluginsService } from './plugins';
@ -32,19 +31,10 @@ export class Server {
private readonly legacy: LegacyService;
private readonly log: Logger;
constructor(
private readonly configService: ConfigService,
logger: LoggerFactory,
private readonly env: Env
) {
this.log = logger.get('server');
this.http = new HttpService(configService.atPath('server', HttpConfig), logger);
const router = new Router('/core');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ version: '0.0.1' }));
this.http.registerRouter(router);
constructor(configService: ConfigService, logger: LoggerFactory, env: Env) {
const core = { env, configService, logger };
this.log = logger.get('server');
this.http = new HttpService(core);
this.plugins = new PluginsService(core);
this.legacy = new LegacyService(core);
this.elasticsearch = new ElasticsearchService(core);
@ -53,31 +43,36 @@ export class Server {
public async setup() {
this.log.debug('setting up server');
// We shouldn't set up http service in two cases:
// 1. If `server.autoListen` is explicitly set to `false`.
// 2. When the process is run as dev cluster master in which case cluster manager
// will fork a dedicated process where http service will be set up instead.
let httpSetup: HttpServiceSetup | undefined;
const httpConfig = await this.configService
.atPath('server', HttpConfig)
.pipe(first())
.toPromise();
if (!this.env.isDevClusterMaster && httpConfig.autoListen) {
httpSetup = await this.http.setup();
}
const httpSetup = await this.http.setup();
this.registerDefaultRoute(httpSetup);
const elasticsearchServiceSetup = await this.elasticsearch.setup();
const pluginsSetup = await this.plugins.setup({
elasticsearch: elasticsearchServiceSetup,
http: httpSetup,
});
await this.legacy.setup({
const coreSetup = {
elasticsearch: elasticsearchServiceSetup,
http: httpSetup,
plugins: pluginsSetup,
});
};
await this.legacy.setup(coreSetup);
return coreSetup;
}
public async start() {
const httpStart = await this.http.start();
const startDeps = {
http: httpStart,
};
await this.legacy.start(startDeps);
return startDeps;
}
public async stop() {
@ -88,4 +83,10 @@ export class Server {
await this.elasticsearch.stop();
await this.http.stop();
}
private registerDefaultRoute(httpSetup: HttpServiceSetup) {
const router = new Router('/core');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ version: '0.0.1' }));
httpSetup.registerRouter(router);
}
}

View file

@ -18,7 +18,8 @@
*/
/** @internal */
export interface CoreService<TSetup = void> {
export interface CoreService<TSetup = void, TStart = void> {
setup(...params: any[]): Promise<TSetup>;
start(...params: any[]): Promise<TStart>;
stop(): Promise<void>;
}

View file

@ -27,6 +27,7 @@ import { createRoot } from '../../../../../test_utils/kbn_server';
// to allow the process to exit naturally
try {
await root.setup();
await root.start();
} finally {
await root.shutdown();
}

View file

@ -24,6 +24,7 @@ beforeAll(async () => {
root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 } });
await root.setup();
await root.start();
kbnTestServer.getKbnServer(root).server.route({
path: '/payload_size_check/test/route',

View file

@ -31,6 +31,7 @@ describe('version_check request filter', function () {
root = kbnTestServer.createRoot();
await root.setup();
await root.start();
kbnTestServer.getKbnServer(root).server.route({
path: '/version_check/test/route',

View file

@ -39,6 +39,7 @@ describe('xsrf request filter', () => {
});
await root.setup();
await root.start();
const kbnServer = kbnTestServer.getKbnServer(root);
kbnServer.server.route({

View file

@ -22,6 +22,7 @@ import { ResponseObject, Server } from 'hapi';
import {
ElasticsearchServiceSetup,
HttpServiceSetup,
HttpServiceStart,
ConfigService,
PluginsServiceSetup,
} from '../../core/server';
@ -77,10 +78,15 @@ export default class KbnServer {
setup: {
core: {
elasticsearch: ElasticsearchServiceSetup;
http?: HttpServiceSetup;
http: HttpServiceSetup;
};
plugins: PluginsServiceSetup;
};
start: {
core: {
http: HttpServiceStart;
};
};
stop: null;
params: {
serverOptions: ElasticsearchServiceSetup;

View file

@ -54,14 +54,19 @@ export default class KbnServer {
this.rootDir = rootDir;
this.settings = settings || {};
const { plugins, http, elasticsearch, serverOptions, handledConfigPaths } = core;
const { setupDeps, startDeps, serverOptions, handledConfigPaths } = core;
this.newPlatform = {
setup: {
core: {
elasticsearch,
http,
elasticsearch: setupDeps.elasticsearch,
http: setupDeps.http,
},
plugins: setupDeps.plugins,
},
start: {
core: {
http: startDeps.http,
},
plugins,
},
stop: null,
params: {

View file

@ -61,6 +61,7 @@ describe('UiExports', function () {
});
await root.setup();
await root.start();
kbnServer = getKbnServer(root);

View file

@ -236,6 +236,7 @@ export async function startTestServers({
const root = createRootWithCorePlugins(kbnSettings);
await root.setup();
await root.start();
const kbnServer = getKbnServer(root);
await kbnServer.server.plugins.elasticsearch.waitUntilReady();