From cbc0e910601bd9518df29c2de79354d740ba9750 Mon Sep 17 00:00:00 2001 From: Todd Kennedy Date: Wed, 29 May 2019 13:00:56 -0700 Subject: [PATCH] [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 --- .../kibana-plugin-server.coresetup.http.md | 1 + .../server/kibana-plugin-server.coresetup.md | 4 + ...server.httpservicesetup.createnewserver.md | 11 + .../kibana-plugin-server.httpservicesetup.md | 11 +- .../core/server/kibana-plugin-server.md | 6 + src/core/server/http/http_server.test.ts | 190 ++++++++++++++++-- src/core/server/http/http_server.ts | 12 +- src/core/server/http/http_service.mock.ts | 8 + .../server/http/http_service.test.mocks.ts | 11 +- src/core/server/http/http_service.test.ts | 45 ++++- src/core/server/http/http_service.ts | 55 ++++- src/core/server/index.ts | 1 + src/core/server/plugins/plugin_context.ts | 1 + src/core/server/server.api.md | 9 +- 14 files changed, 323 insertions(+), 42 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-server.coresetup.http.md index 8c547ca2a42a9..46d44d01bd8b0 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.http.md @@ -12,5 +12,6 @@ http: { registerOnRequest: HttpServiceSetup['registerOnRequest']; getBasePathFor: HttpServiceSetup['getBasePathFor']; setBasePathFor: HttpServiceSetup['setBasePathFor']; + createNewServer: HttpServiceSetup['createNewServer']; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index fe6d3ee71edc8..d64a22be7c4b2 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -17,5 +17,9 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {`

` adminClient$: Observable<ClusterClient>;`

` dataClient$: Observable<ClusterClient>;`

` } | | +<<<<<<< HEAD | [http](./kibana-plugin-server.coresetup.http.md) | {`

` registerAuth: HttpServiceSetup['registerAuth'];`

` registerOnRequest: HttpServiceSetup['registerOnRequest'];`

` getBasePathFor: HttpServiceSetup['getBasePathFor'];`

` setBasePathFor: HttpServiceSetup['setBasePathFor'];`

` } | | +======= +| [http](./kibana-plugin-server.coresetup.http.md) | {`

` registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];`

` registerAuth: HttpServiceSetup['registerAuth'];`

` registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];`

` getBasePathFor: HttpServiceSetup['getBasePathFor'];`

` setBasePathFor: HttpServiceSetup['setBasePathFor'];`

` createNewServer: HttpServiceSetup['createNewServer'];`

` } | | +>>>>>>> 461a6c0f93... [feat] create additional http servers (#36804) diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md new file mode 100644 index 0000000000000..e33f1c51abee9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [createNewServer](./kibana-plugin-server.httpservicesetup.createnewserver.md) + +## HttpServiceSetup.createNewServer property + +Signature: + +```typescript +createNewServer: (cfg: Partial) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index 36335c0d5f4cd..a1b77ba466399 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -2,11 +2,18 @@ [Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) -## HttpServiceSetup type +## HttpServiceSetup interface Signature: ```typescript -export declare type HttpServiceSetup = HttpServerSetup; +export interface HttpServiceSetup extends HttpServerSetup ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [createNewServer](./kibana-plugin-server.httpservicesetup.createnewserver.md) | (cfg: Partial<HttpConfig>) => Promise<HttpServerSetup> | | + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index c078a5fdc983f..dbfd3bd806836 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -29,6 +29,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start 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. | @@ -48,8 +49,13 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [Headers](./kibana-plugin-server.headers.md) | | +<<<<<<< HEAD | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | | [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md) | | +======= +| [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | | +| [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | | +>>>>>>> 461a6c0f93... [feat] create additional http servers (#36804) | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server 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. | diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 21e1193471972..39e40fd756736 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -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; }); @@ -508,7 +508,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { const { registerRouter, server: innerServer } = await server.setup(config); registerRouter(router); - await server.start(configWithBasePath); + await server.start(); innerServerListener = innerServer.listener; }); @@ -571,10 +571,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 +610,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 +634,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 +668,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) @@ -708,3 +708,149 @@ test('#setBasePathFor() cannot be set twice for one request', async () => { `"Request basePath was previously set. Setting multiple times is not supported."` ); }); +const cookieOptions = { + name: 'sid', + encryptionKey: 'something_at_least_32_characters', + validate: () => true, + isSecure: false, +}; + +test('Should enable auth for a route by default if registerAuth has been called', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({})); + registerRouter(router); + + const authenticate = jest + .fn() + .mockImplementation((req, sessionStorage, t) => t.authenticated({})); + await registerAuth(authenticate, cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(authenticate).toHaveBeenCalledTimes(1); +}); + +test('Should support disabling auth for a route', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, authRequired: false }, async (req, res) => res.ok({})); + registerRouter(router); + const authenticate = jest.fn(); + await registerAuth(authenticate, cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(authenticate).not.toHaveBeenCalled(); +}); + +describe('#auth.isAuthenticated()', () => { + it('returns true if has been authorized', async () => { + const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => + res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + ); + registerRouter(router); + + await registerAuth((req, sessionStorage, t) => t.authenticated({}), cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { isAuthenticated: true }); + }); + + it('returns false if has not been authorized', async () => { + const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, authRequired: false }, async (req, res) => + res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + ); + registerRouter(router); + + await registerAuth((req, sessionStorage, t) => t.authenticated({}), cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { isAuthenticated: false }); + }); + + it('returns false if no authorization mechanism has been registered', async () => { + const { registerRouter, server: innerServer, auth } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, authRequired: false }, async (req, res) => + res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { isAuthenticated: false }); + }); +}); + +describe('#auth.get()', () => { + it('Should return authenticated status and allow associate auth state with request', async () => { + const user = { id: '42' }; + const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config); + await registerAuth((req, sessionStorage, t) => { + sessionStorage.set({ value: user }); + return t.authenticated(user); + }, cookieOptions); + + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok(auth.get(req))); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { state: user, status: 'authenticated' }); + }); + + it('Should return correct authentication unknown status', async () => { + const { registerRouter, server: innerServer, auth } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok(auth.get(req))); + + registerRouter(router); + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { status: 'unknown' }); + }); + + it('Should return correct unauthenticated status', async () => { + const authenticate = jest.fn(); + + const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config); + await registerAuth(authenticate, cookieOptions); + const router = new Router(''); + router.get({ path: '/', validate: false, authRequired: false }, async (req, res) => + res.ok(auth.get(req)) + ); + + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { status: 'unauthenticated' }); + + expect(authenticate).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 6dbae8a14d601..462bc2d13c8e6 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -55,6 +55,7 @@ export interface HttpServerSetup { export class HttpServer { private server?: Server; + private config?: HttpConfig; private registeredRouters = new Set(); 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,7 +122,7 @@ 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'); } @@ -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() { diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 289eae0990531..bb879f639e6bc 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -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,12 @@ const createSetupContractMock = () => { setBasePathFor: jest.fn(), // we can mock some hapi server method when we need it server: {} as Server, + auth: { + get: jest.fn(), + isAuthenticated: jest.fn(), + }, + createNewServer: async (cfg: Partial): Promise => + ({} as HttpServerSetup), }; return setupContract; }; diff --git a/src/core/server/http/http_service.test.mocks.ts b/src/core/server/http/http_service.test.mocks.ts index a0d7ff5069eb0..c147944f2b7d8 100644 --- a/src/core/server/http/http_service.test.mocks.ts +++ b/src/core/server/http/http_service.test.mocks.ts @@ -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, + }; +}); diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index 7b3fd024b477c..16f946ffcc7ae 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -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 () => { diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 3a9fcb7be54e7..fec3774e2f366 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -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) => Promise; +} /** @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 { private readonly httpServer: HttpServer; + private readonly secondaryServers: Map = new Map(); private readonly httpsRedirectServer: HttpsRedirectServer; private readonly config$: Observable; 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('server') @@ -69,7 +75,12 @@ export class HttpService implements CoreService 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) { + 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 s.stop())); + this.secondaryServers.clear(); } } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index d3223674eb00c..02452f7b0756f 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -83,6 +83,7 @@ export interface CoreSetup { registerOnRequest: HttpServiceSetup['registerOnRequest']; getBasePathFor: HttpServiceSetup['getBasePathFor']; setBasePathFor: HttpServiceSetup['setBasePathFor']; + createNewServer: HttpServiceSetup['createNewServer']; }; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index e533ac0cfe9ac..ddd65b198baa6 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -121,6 +121,7 @@ export function createPluginSetupContext( registerOnRequest: deps.http.registerOnRequest, getBasePathFor: deps.http.getBasePathFor, setBasePathFor: deps.http.setBasePathFor, + createNewServer: deps.http.createNewServer, }, }; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 821d85a3812f3..b9148a2b94a57 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -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; // 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) => Promise; +} // @public (undocumented) export interface HttpServiceStart {