[7.x] [feat] create additional http servers (#36804) (#37517)

* [feat] create additional http servers (#36804)

* [feat] create additional http servers

allow for additional http servers to be created, tracked and returned

* respond to pr feedback

* tweak test

* update documentation

* destructure port, remove unnecessary imports

* [fix] export correct type

* [feat] expose createNewServer to plugins

* [fix] respond to pr feedback

* todo: add schema validation & integration test

* use reach

* [fix] use validateKey to validate partial

* [fix] change config shadowing

* check kibana port & prevent shadowing

* centralize start/stop for servers, add integration test

* remove unnecessary property

* never forget your await

* remove option to pass config into start

* fix pr feedback

* fix documentation

* fix test failures

* [fix] failing negation on merge
This commit is contained in:
Todd Kennedy 2019-05-31 10:13:09 -07:00 committed by GitHub
parent 95c362805e
commit c4a9ade6ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 244 additions and 126 deletions

View file

@ -12,5 +12,6 @@ http: {
registerOnRequest: HttpServiceSetup['registerOnRequest'];
getBasePathFor: HttpServiceSetup['getBasePathFor'];
setBasePathFor: HttpServiceSetup['setBasePathFor'];
createNewServer: HttpServiceSetup['createNewServer'];
};
```

View file

@ -1,21 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &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 |
| --- | --- | --- |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>{`<p/>` adminClient$: Observable&lt;ClusterClient&gt;;`<p/>` dataClient$: Observable&lt;ClusterClient&gt;;`<p/>` }</code> | |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>{`<p/>` registerAuth: HttpServiceSetup['registerAuth'];`<p/>` registerOnRequest: HttpServiceSetup['registerOnRequest'];`<p/>` getBasePathFor: HttpServiceSetup['getBasePathFor'];`<p/>` setBasePathFor: HttpServiceSetup['setBasePathFor'];`<p/>` }</code> | |
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &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 |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>{`<p/>` adminClient\$: Observable&lt;ClusterClient&gt;;`<p/>` dataClient\$: Observable&lt;ClusterClient&gt;;`<p/>` }</code> | |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>{`<p/>` registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];`<p/>` registerAuth: HttpServiceSetup['registerAuth'];`<p/>` registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];`<p/>` getBasePathFor: HttpServiceSetup['getBasePathFor'];`<p/>` setBasePathFor: HttpServiceSetup['setBasePathFor'];`<p/>` createNewServer: HttpServiceSetup['createNewServer'];`<p/>` }</code> | |

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; [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) &gt; [createNewServer](./kibana-plugin-server.httpservicesetup.createnewserver.md)
## HttpServiceSetup.createNewServer property
<b>Signature:</b>
```typescript
createNewServer: (cfg: Partial<HttpConfig>) => Promise<HttpServerSetup>;
```

View file

@ -2,11 +2,18 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md)
## HttpServiceSetup type
## HttpServiceSetup interface
<b>Signature:</b>
```typescript
export declare type HttpServiceSetup = HttpServerSetup;
export interface HttpServiceSetup extends HttpServerSetup
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [createNewServer](./kibana-plugin-server.httpservicesetup.createnewserver.md) | <code>(cfg: Partial&lt;HttpConfig&gt;) =&gt; Promise&lt;HttpServerSetup&gt;</code> | |

View file

@ -1,55 +1,55 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md)
## kibana-plugin-server package
The Kibana Core APIs for server-side plugins.
A plugin's `server/index` file must contain a named import, `plugin`<!-- -->, that implements [PluginInitializer](./kibana-plugin-server.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-server.plugin.md)<!-- -->.
The plugin integrates with the core system via lifecycle events: `setup`<!-- -->, `start`<!-- -->, and `stop`<!-- -->. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-server.coresetup.md) or [CoreStart](./kibana-plugin-server.corestart.md)<!-- -->) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked.
## Classes
| Class | Description |
| --- | --- |
| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via <code>asScoped(...)</code>). |
| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | |
| [Router](./kibana-plugin-server.router.md) | |
| [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" <code>ClusterClient</code> but exposes additional <code>callAsCurrentUser</code> method that doesn't use credentials of the Kibana internal user (as <code>callAsInternalUser</code> does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API |
## Interfaces
| Interface | Description |
| --- | --- |
| [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) | Context passed to the plugins <code>setup</code> method. |
| [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins <code>start</code> method. |
| [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. |
| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | |
| [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | |
| [InternalCoreStart](./kibana-plugin-server.internalcorestart.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 |
| [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) | A tool set defining an outcome of OnRequest interceptor for incoming request. |
| [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. |
| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | |
| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | |
## Type Aliases
| Type Alias | Description |
| --- | --- |
| [APICaller](./kibana-plugin-server.apicaller.md) | |
| [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | |
| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | |
| [Headers](./kibana-plugin-server.headers.md) | |
| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | |
| [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md) | |
| [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>server</code> directory should conform to this interface. |
| [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. |
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md)
## kibana-plugin-server package
The Kibana Core APIs for server-side plugins.
A plugin's `server/index` file must contain a named import, `plugin`<!-- -->, that implements [PluginInitializer](./kibana-plugin-server.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-server.plugin.md)<!-- -->.
The plugin integrates with the core system via lifecycle events: `setup`<!-- -->, `start`<!-- -->, and `stop`<!-- -->. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-server.coresetup.md) or [CoreStart](./kibana-plugin-server.corestart.md)<!-- -->) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked.
## Classes
| Class | Description |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via <code>asScoped(...)</code>). |
| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | |
| [Router](./kibana-plugin-server.router.md) | |
| [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" <code>ClusterClient</code> but exposes additional <code>callAsCurrentUser</code> method that doesn't use credentials of the Kibana internal user (as <code>callAsInternalUser</code> does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API |
## Interfaces
| Interface | Description |
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| [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) | Context passed to the plugins <code>setup</code> method. |
| [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins <code>start</code> method. |
| [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. |
| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | |
| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | |
| [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | |
| [InternalCoreStart](./kibana-plugin-server.internalcorestart.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 |
| [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) | A tool set defining an outcome of OnRequest interceptor for incoming request. |
| [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. |
| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | |
| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | |
## Type Aliases
| Type Alias | Description |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| [APICaller](./kibana-plugin-server.apicaller.md) | |
| [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | |
| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | |
| [Headers](./kibana-plugin-server.headers.md) | |
| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | |
| [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md) | |
| [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>server</code> directory should conform to this interface. |
| [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. |

View file

@ -58,7 +58,7 @@ test('listening after started', async () => {
expect(server.isListening()).toBe(false);
await server.setup(config);
await server.start(config);
await server.start();
expect(server.isListening()).toBe(true);
});
@ -72,7 +72,7 @@ test('200 OK with body', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/foo/')
@ -92,7 +92,7 @@ test('202 Accepted with body', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/foo/')
@ -112,7 +112,7 @@ test('204 No content', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/foo/')
@ -134,7 +134,7 @@ test('400 Bad request with error', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/foo/')
@ -164,7 +164,7 @@ test('valid params', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/foo/some-string')
@ -194,7 +194,7 @@ test('invalid params', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/foo/some-string')
@ -227,7 +227,7 @@ test('valid query', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/foo/?bar=test&quux=123')
@ -257,7 +257,7 @@ test('invalid query', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/foo/?bar=test')
@ -290,7 +290,7 @@ test('valid body', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.post('/foo/')
@ -324,7 +324,7 @@ test('invalid body', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.post('/foo/')
@ -357,7 +357,7 @@ test('handles putting', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.put('/foo/')
@ -388,7 +388,7 @@ test('handles deleting', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.delete('/foo/3')
@ -414,7 +414,7 @@ test('filtered headers', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/foo/?bar=quux')
@ -444,10 +444,10 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => {
res.ok({ key: 'value:/foo' })
);
const { registerRouter, server: innerServer } = await server.setup(config);
const { registerRouter, server: innerServer } = await server.setup(configWithBasePath);
registerRouter(router);
await server.start(configWithBasePath);
await server.start();
innerServerListener = innerServer.listener;
});
@ -489,8 +489,8 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => {
});
describe('with `basepath: /bar` and `rewriteBasePath: true`', () => {
let configWithBasePath: HttpConfig;
let innerServerListener: Server;
let configWithBasePath: HttpConfig;
beforeEach(async () => {
configWithBasePath = {
@ -498,17 +498,16 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => {
basePath: '/bar',
rewriteBasePath: true,
} as HttpConfig;
const router = new Router('/');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ key: 'value:/' }));
router.get({ path: '/foo', validate: false }, async (req, res) =>
res.ok({ key: 'value:/foo' })
);
const { registerRouter, server: innerServer } = await server.setup(config);
const { registerRouter, server: innerServer } = await server.setup(configWithBasePath);
registerRouter(router);
await server.start(configWithBasePath);
await server.start();
innerServerListener = innerServer.listener;
});
@ -571,10 +570,10 @@ describe('with defined `redirectHttpFromPort`', () => {
const router = new Router('/');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ key: 'value:/' }));
const { registerRouter } = await server.setup(config);
const { registerRouter } = await server.setup(configWithSSL);
registerRouter(router);
await server.start(configWithSSL);
await server.start();
});
});
@ -610,7 +609,7 @@ test('registers onRequest interceptor several times', async () => {
});
test('throws an error if starts without set up', async () => {
await expect(server.start(config)).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(server.start()).rejects.toThrowErrorMatchingInlineSnapshot(
`"Http server is not setup up yet"`
);
});
@ -634,7 +633,7 @@ test('#getBasePathFor() returns base path associated with an incoming request',
router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: getBasePathFor(req) }));
registerRouter(router);
await server.start(config);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200)
@ -668,7 +667,7 @@ test('#getBasePathFor() is based on server base path', async () => {
);
registerRouter(router);
await server.start(configWithBasePath);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200)

View file

@ -55,6 +55,7 @@ export interface HttpServerSetup {
export class HttpServer {
private server?: Server;
private config?: HttpConfig;
private registeredRouters = new Set<Router>();
private authRegistered = false;
private basePathCache = new WeakMap<
@ -102,6 +103,7 @@ export class HttpServer {
public setup(config: HttpConfig): HttpServerSetup {
const serverOptions = getServerOptions(config);
this.server = createServer(serverOptions);
this.config = config;
return {
options: serverOptions,
@ -120,13 +122,13 @@ export class HttpServer {
};
}
public async start(config: HttpConfig) {
public async start() {
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);
this.setupBasePathRewrite(this.server);
for (const router of this.registeredRouters) {
for (const route of router.getRoutes()) {
@ -139,12 +141,8 @@ export class HttpServer {
}
await this.server.start();
this.log.debug(
`http server running at ${this.server.info.uri}${
config.rewriteBasePath ? config.basePath : ''
}`
);
const serverPath = this.config!.rewriteBasePath || this.config!.basePath || '';
this.log.debug(`http server running at ${this.server.info.uri}${serverPath}`);
}
public async stop() {
@ -157,12 +155,12 @@ export class HttpServer {
this.server = undefined;
}
private setupBasePathRewrite(server: Server, config: HttpConfig) {
if (config.basePath === undefined || !config.rewriteBasePath) {
private setupBasePathRewrite(server: Server) {
if (this.config!.basePath === undefined || !this.config!.rewriteBasePath) {
return;
}
const basePath = config.basePath;
const basePath = this.config!.basePath;
server.ext('onRequest', (request, responseToolkit) => {
const newURL = modifyUrl(request.url.href!, urlParts => {
if (urlParts.pathname != null && urlParts.pathname.startsWith(basePath)) {
@ -171,7 +169,6 @@ export class HttpServer {
return {};
}
});
if (!newURL) {
return responseToolkit
.response('Not Found')

View file

@ -19,6 +19,8 @@
import { Server, ServerOptions } from 'hapi';
import { HttpService } from './http_service';
import { HttpConfig } from './http_config';
import { HttpServerSetup } from './http_server';
const createSetupContractMock = () => {
const setupContract = {
@ -30,6 +32,8 @@ const createSetupContractMock = () => {
setBasePathFor: jest.fn(),
// we can mock some hapi server method when we need it
server: {} as Server,
createNewServer: async (cfg: Partial<HttpConfig>): Promise<HttpServerSetup> =>
({} as HttpServerSetup),
};
return setupContract;
};

View file

@ -19,6 +19,11 @@
export const mockHttpServer = jest.fn();
jest.mock('./http_server', () => ({
HttpServer: mockHttpServer,
}));
jest.mock('./http_server', () => {
const realHttpServer = jest.requireActual('./http_server');
return {
...realHttpServer,
HttpServer: mockHttpServer,
};
});

View file

@ -76,6 +76,46 @@ test('creates and sets up http server', async () => {
expect(httpServer.start).toHaveBeenCalledTimes(1);
});
// this is an integration test!
test('creates and sets up second http server', async () => {
const configService = createConfigService({
host: 'localhost',
port: 1234,
});
const { HttpServer } = jest.requireActual('./http_server');
mockHttpServer.mockImplementation((...args) => new HttpServer(...args));
const service = new HttpService({ configService, env, logger });
const serverSetup = await service.setup();
const cfg = { port: 2345 };
await serverSetup.createNewServer(cfg);
const server = await service.start();
expect(server.isListening()).toBeTruthy();
expect(server.isListening(cfg.port)).toBeTruthy();
try {
await serverSetup.createNewServer(cfg);
} catch (err) {
expect(err.message).toBe('port 2345 is already in use');
}
try {
await serverSetup.createNewServer({ port: 1234 });
} catch (err) {
expect(err.message).toBe('port 1234 is already in use');
}
try {
await serverSetup.createNewServer({ host: 'example.org' });
} catch (err) {
expect(err.message).toBe('port must be defined');
}
await service.stop();
expect(server.isListening()).toBeFalsy();
expect(server.isListening(cfg.port)).toBeFalsy();
});
test('logs error if already set up', async () => {
const configService = createConfigService();
@ -153,8 +193,9 @@ test('returns http server contract on setup', async () => {
}));
const service = new HttpService({ configService, env, logger });
expect(await service.setup()).toBe(httpServer);
const { createNewServer, ...setupHttpServer } = await service.setup();
expect(createNewServer).toBeDefined();
expect(setupHttpServer).toEqual(httpServer);
});
test('does not start http server if process is dev cluster master', async () => {

View file

@ -20,15 +20,18 @@
import { Observable, Subscription } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { LoggerFactory } from '../logging';
import { CoreService } from '../../types';
import { Logger } from '../logging';
import { CoreContext } from '../core_context';
import { HttpConfig, HttpConfigType } from './http_config';
import { HttpConfig, HttpConfigType, config as httpConfig } from './http_config';
import { HttpServer, HttpServerSetup } from './http_server';
import { HttpsRedirectServer } from './https_redirect_server';
/** @public */
export type HttpServiceSetup = HttpServerSetup;
export interface HttpServiceSetup extends HttpServerSetup {
createNewServer: (cfg: Partial<HttpConfig>) => Promise<HttpServerSetup>;
}
/** @public */
export interface HttpServiceStart {
/** Indicates if http server is listening on a port */
@ -38,13 +41,16 @@ export interface HttpServiceStart {
/** @internal */
export class HttpService implements CoreService<HttpServiceSetup, HttpServiceStart> {
private readonly httpServer: HttpServer;
private readonly secondaryServers: Map<number, HttpServer> = new Map();
private readonly httpsRedirectServer: HttpsRedirectServer;
private readonly config$: Observable<HttpConfig>;
private configSubscription?: Subscription;
private readonly logger: LoggerFactory;
private readonly log: Logger;
constructor(private readonly coreContext: CoreContext) {
this.logger = coreContext.logger;
this.log = coreContext.logger.get('http');
this.config$ = coreContext.configService
.atPath<HttpConfigType>('server')
@ -69,7 +75,12 @@ export class HttpService implements CoreService<HttpServiceSetup, HttpServiceSta
const config = await this.config$.pipe(first()).toPromise();
return this.httpServer.setup(config);
const httpSetup = (this.httpServer.setup(config) || {}) as HttpServiceSetup;
const setup = {
...httpSetup,
...{ createNewServer: this.createServer.bind(this) },
};
return setup;
}
public async start() {
@ -86,14 +97,46 @@ export class HttpService implements CoreService<HttpServiceSetup, HttpServiceSta
await this.httpsRedirectServer.start(config);
}
await this.httpServer.start(config);
await this.httpServer.start();
await Promise.all([...this.secondaryServers.values()].map(server => server.start()));
}
return {
isListening: () => this.httpServer.isListening(),
isListening: (port = 0) => {
const server = this.secondaryServers.get(port);
if (server) return server.isListening();
return this.httpServer.isListening();
},
};
}
private async createServer(cfg: Partial<HttpConfig>) {
const { port } = cfg;
const config = await this.config$.pipe(first()).toPromise();
if (!port) {
throw new Error('port must be defined');
}
// verify that main server and none of the secondary servers are already using this port
if (this.secondaryServers.has(port) || config.port === port) {
throw new Error(`port ${port} is already in use`);
}
for (const [key, val] of Object.entries(cfg)) {
httpConfig.schema.validateKey(key, val);
}
const baseConfig = await this.config$.pipe(first()).toPromise();
const finalConfig = { ...baseConfig, ...cfg };
const log = this.logger.get('http', `server:${port}`);
const httpServer = new HttpServer(log);
const httpSetup = await httpServer.setup(finalConfig);
this.secondaryServers.set(port, httpServer);
return httpSetup;
}
public async stop() {
if (this.configSubscription === undefined) {
return;
@ -104,5 +147,7 @@ export class HttpService implements CoreService<HttpServiceSetup, HttpServiceSta
await this.httpServer.stop();
await this.httpsRedirectServer.stop();
await Promise.all([...this.secondaryServers.values()].map(s => s.stop()));
this.secondaryServers.clear();
}
}

View file

@ -83,6 +83,7 @@ export interface CoreSetup {
registerOnRequest: HttpServiceSetup['registerOnRequest'];
getBasePathFor: HttpServiceSetup['getBasePathFor'];
setBasePathFor: HttpServiceSetup['setBasePathFor'];
createNewServer: HttpServiceSetup['createNewServer'];
};
}

View file

@ -121,6 +121,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
registerOnRequest: deps.http.registerOnRequest,
getBasePathFor: deps.http.getBasePathFor,
setBasePathFor: deps.http.setBasePathFor,
createNewServer: deps.http.createNewServer,
},
};
}

View file

@ -4,6 +4,7 @@
```ts
import { ByteSizeValue } from '@kbn/config-schema';
import { ConfigOptions } from 'elasticsearch';
import { Duration } from 'moment';
import { ObjectType } from '@kbn/config-schema';
@ -87,6 +88,7 @@ export interface CoreSetup {
registerOnRequest: HttpServiceSetup['registerOnRequest'];
getBasePathFor: HttpServiceSetup['getBasePathFor'];
setBasePathFor: HttpServiceSetup['setBasePathFor'];
createNewServer: HttpServiceSetup['createNewServer'];
};
}
@ -132,7 +134,12 @@ export type Headers = Record<string, string | string[] | undefined>;
// Warning: (ae-forgotten-export) The symbol "HttpServerSetup" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export type HttpServiceSetup = HttpServerSetup;
export interface HttpServiceSetup extends HttpServerSetup {
// Warning: (ae-forgotten-export) The symbol "HttpConfig" needs to be exported by the entry point index.d.ts
//
// (undocumented)
createNewServer: (cfg: Partial<HttpConfig>) => Promise<HttpServerSetup>;
}
// @public (undocumented)
export interface HttpServiceStart {