diff --git a/src/core/server/config/__tests__/__mocks__/env.ts b/src/core/server/config/__tests__/__mocks__/env.ts index e86cd9aab77cd..fe33fd32f4648 100644 --- a/src/core/server/config/__tests__/__mocks__/env.ts +++ b/src/core/server/config/__tests__/__mocks__/env.ts @@ -21,33 +21,14 @@ import { EnvOptions } from '../../env'; -interface MockEnvOptions { - config?: string; - kbnServer?: any; - mode?: EnvOptions['mode']['name']; - packageInfo?: Partial; -} - -export function getEnvOptions({ - config, - kbnServer, - mode = 'development', - packageInfo = {}, -}: MockEnvOptions = {}): EnvOptions { +export function getEnvOptions(options: Partial = {}): EnvOptions { return { - config, - kbnServer, - mode: { - dev: mode === 'development', - name: mode, - prod: mode === 'production', - }, - packageInfo: { - branch: 'some-branch', - buildNum: 1, - buildSha: 'some-sha-256', - version: 'some-version', - ...packageInfo, + configs: options.configs || [], + cliArgs: { + dev: true, + ...(options.cliArgs || {}), }, + isDevClusterMaster: + options.isDevClusterMaster !== undefined ? options.isDevClusterMaster : false, }; } diff --git a/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap b/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap new file mode 100644 index 0000000000000..db2917da5406f --- /dev/null +++ b/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap @@ -0,0 +1,149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`correctly creates default environment in dev mode.: env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "dev": true, + "someArg": 1, + "someOtherArg": "2", + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/test/cwd/config/kibana.yml", + ], + "corePluginsDir": "/test/cwd/core_plugins", + "homeDir": "/test/cwd", + "isDevClusterMaster": true, + "legacy": EventEmitter { + "_events": Object {}, + "_eventsCount": 0, + "_maxListeners": undefined, + "domain": null, + }, + "logDir": "/test/cwd/log", + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": "some-branch", + "buildNum": 9007199254740991, + "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "version": "some-version", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + +exports[`correctly creates default environment in prod distributable mode.: env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "dev": false, + "someArg": 1, + "someOtherArg": "2", + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "corePluginsDir": "/test/cwd/core_plugins", + "homeDir": "/test/cwd", + "isDevClusterMaster": false, + "legacy": EventEmitter { + "_events": Object {}, + "_eventsCount": 0, + "_maxListeners": undefined, + "domain": null, + }, + "logDir": "/test/cwd/log", + "mode": Object { + "dev": false, + "name": "production", + "prod": true, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 100, + "buildSha": "feature-v1-build-sha", + "version": "v1", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + +exports[`correctly creates default environment in prod non-distributable mode.: env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "dev": false, + "someArg": 1, + "someOtherArg": "2", + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "corePluginsDir": "/test/cwd/core_plugins", + "homeDir": "/test/cwd", + "isDevClusterMaster": false, + "legacy": EventEmitter { + "_events": Object {}, + "_eventsCount": 0, + "_maxListeners": undefined, + "domain": null, + }, + "logDir": "/test/cwd/log", + "mode": Object { + "dev": false, + "name": "production", + "prod": true, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 9007199254740991, + "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "version": "v1", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + +exports[`correctly creates environment with constructor.: env properties 1`] = ` +Env { + "binDir": "/some/home/dir/bin", + "cliArgs": Object { + "dev": false, + "someArg": 1, + "someOtherArg": "2", + }, + "configDir": "/some/home/dir/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "corePluginsDir": "/some/home/dir/core_plugins", + "homeDir": "/some/home/dir", + "isDevClusterMaster": false, + "legacy": EventEmitter { + "_events": Object {}, + "_eventsCount": 0, + "_maxListeners": undefined, + "domain": null, + }, + "logDir": "/some/home/dir/log", + "mode": Object { + "dev": false, + "name": "production", + "prod": true, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 100, + "buildSha": "feature-v1-build-sha", + "version": "v1", + }, + "staticFilesDir": "/some/home/dir/ui", +} +`; diff --git a/src/core/server/config/__tests__/config_service.test.ts b/src/core/server/config/__tests__/config_service.test.ts index 1cac0d4ef8d0e..2dbf99587f334 100644 --- a/src/core/server/config/__tests__/config_service.test.ts +++ b/src/core/server/config/__tests__/config_service.test.ts @@ -18,8 +18,13 @@ */ /* tslint:disable max-classes-per-file */ + import { BehaviorSubject } from 'rxjs'; import { first } from 'rxjs/operators'; + +const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); +jest.mock('../../../../utils/package_json', () => ({ pkg: mockPackage })); + import { schema, Type, TypeOf } from '../schema'; import { ConfigService, ObjectToRawConfigAdapter } from '..'; @@ -161,21 +166,19 @@ test('tracks unhandled paths', async () => { }); test('correctly passes context', async () => { - const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ foo: {} })); + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; - const env = new Env( - '/kibana', - getEnvOptions({ - mode: 'development', - packageInfo: { - branch: 'feature-v1', - buildNum: 100, - buildSha: 'feature-v1-build-sha', - version: 'v1', - }, - }) - ); + const env = new Env('/kibana', getEnvOptions()); + const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ foo: {} })); const configService = new ConfigService(config$, env, logger); const configs = configService.atPath( 'foo', diff --git a/src/core/server/config/__tests__/env.test.ts b/src/core/server/config/__tests__/env.test.ts index 8707bb2f2a2f7..26163c82c8464 100644 --- a/src/core/server/config/__tests__/env.test.ts +++ b/src/core/server/config/__tests__/env.test.ts @@ -29,76 +29,82 @@ jest.mock('path', () => ({ }, })); +const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); +jest.mock('../../../../utils/package_json', () => ({ pkg: mockPackage })); + import { Env } from '../env'; -import { getEnvOptions } from './__mocks__/env'; - -test('correctly creates default environment with empty options.', () => { - const envOptions = getEnvOptions(); - const defaultEnv = Env.createDefault(envOptions); - - expect(defaultEnv.homeDir).toEqual('/test/cwd'); - expect(defaultEnv.configDir).toEqual('/test/cwd/config'); - expect(defaultEnv.corePluginsDir).toEqual('/test/cwd/core_plugins'); - expect(defaultEnv.binDir).toEqual('/test/cwd/bin'); - expect(defaultEnv.logDir).toEqual('/test/cwd/log'); - expect(defaultEnv.staticFilesDir).toEqual('/test/cwd/ui'); - - expect(defaultEnv.getConfigFile()).toEqual('/test/cwd/config/kibana.yml'); - expect(defaultEnv.getLegacyKbnServer()).toBeUndefined(); - expect(defaultEnv.getMode()).toEqual(envOptions.mode); - expect(defaultEnv.getPackageInfo()).toEqual(envOptions.packageInfo); + +test('correctly creates default environment in dev mode.', () => { + mockPackage.raw = { + branch: 'some-branch', + version: 'some-version', + }; + + const defaultEnv = Env.createDefault({ + cliArgs: { dev: true, someArg: 1, someOtherArg: '2' }, + configs: ['/test/cwd/config/kibana.yml'], + isDevClusterMaster: true, + }); + + expect(defaultEnv).toMatchSnapshot('env properties'); }); -test('correctly creates default environment with options overrides.', () => { - const mockEnvOptions = getEnvOptions({ - config: '/some/other/path/some-kibana.yml', - kbnServer: {}, - mode: 'production', - packageInfo: { - branch: 'feature-v1', - buildNum: 100, - buildSha: 'feature-v1-build-sha', - version: 'v1', +test('correctly creates default environment in prod distributable mode.', () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', }, + }; + + const defaultEnv = Env.createDefault({ + cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, + configs: ['/some/other/path/some-kibana.yml'], + isDevClusterMaster: false, }); - const defaultEnv = Env.createDefault(mockEnvOptions); - - expect(defaultEnv.homeDir).toEqual('/test/cwd'); - expect(defaultEnv.configDir).toEqual('/test/cwd/config'); - expect(defaultEnv.corePluginsDir).toEqual('/test/cwd/core_plugins'); - expect(defaultEnv.binDir).toEqual('/test/cwd/bin'); - expect(defaultEnv.logDir).toEqual('/test/cwd/log'); - expect(defaultEnv.staticFilesDir).toEqual('/test/cwd/ui'); - - expect(defaultEnv.getConfigFile()).toEqual(mockEnvOptions.config); - expect(defaultEnv.getLegacyKbnServer()).toBe(mockEnvOptions.kbnServer); - expect(defaultEnv.getMode()).toEqual(mockEnvOptions.mode); - expect(defaultEnv.getPackageInfo()).toEqual(mockEnvOptions.packageInfo); + + expect(defaultEnv).toMatchSnapshot('env properties'); }); -test('correctly creates environment with constructor.', () => { - const mockEnvOptions = getEnvOptions({ - config: '/some/other/path/some-kibana.yml', - mode: 'production', - packageInfo: { - branch: 'feature-v1', - buildNum: 100, - buildSha: 'feature-v1-build-sha', - version: 'v1', +test('correctly creates default environment in prod non-distributable mode.', () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: false, + number: 100, + sha: 'feature-v1-build-sha', }, + }; + + const defaultEnv = Env.createDefault({ + cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, + configs: ['/some/other/path/some-kibana.yml'], + isDevClusterMaster: false, }); - const defaultEnv = new Env('/some/home/dir', mockEnvOptions); + expect(defaultEnv).toMatchSnapshot('env properties'); +}); - expect(defaultEnv.homeDir).toEqual('/some/home/dir'); - expect(defaultEnv.configDir).toEqual('/some/home/dir/config'); - expect(defaultEnv.corePluginsDir).toEqual('/some/home/dir/core_plugins'); - expect(defaultEnv.binDir).toEqual('/some/home/dir/bin'); - expect(defaultEnv.logDir).toEqual('/some/home/dir/log'); - expect(defaultEnv.staticFilesDir).toEqual('/some/home/dir/ui'); +test('correctly creates environment with constructor.', () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; + + const env = new Env('/some/home/dir', { + cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, + configs: ['/some/other/path/some-kibana.yml'], + isDevClusterMaster: false, + }); - expect(defaultEnv.getConfigFile()).toEqual(mockEnvOptions.config); - expect(defaultEnv.getLegacyKbnServer()).toBeUndefined(); - expect(defaultEnv.getMode()).toEqual(mockEnvOptions.mode); - expect(defaultEnv.getPackageInfo()).toEqual(mockEnvOptions.packageInfo); + expect(env).toMatchSnapshot('env properties'); }); diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index a918bb588e94c..bd839e51cbcf5 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -138,13 +138,12 @@ export class ConfigService { ); } - const environmentMode = this.env.getMode(); const config = ConfigClass.schema.validate( rawConfig, { - dev: environmentMode.dev, - prod: environmentMode.prod, - ...this.env.getPackageInfo(), + dev: this.env.mode.dev, + prod: this.env.mode.prod, + ...this.env.packageInfo, }, namespace ); diff --git a/src/core/server/config/env.ts b/src/core/server/config/env.ts index 87e4b6567120b..56d6c1ae94a0c 100644 --- a/src/core/server/config/env.ts +++ b/src/core/server/config/env.ts @@ -17,10 +17,11 @@ * under the License. */ +import { EventEmitter } from 'events'; import { resolve } from 'path'; import process from 'process'; -import { LegacyKbnServer } from '../legacy_compat'; +import { pkg } from '../../../utils/package_json'; interface PackageInfo { version: string; @@ -36,11 +37,9 @@ interface EnvironmentMode { } export interface EnvOptions { - config?: string; - kbnServer?: any; - packageInfo: PackageInfo; - mode: EnvironmentMode; - [key: string]: any; + configs: string[]; + cliArgs: Record; + isDevClusterMaster: boolean; } export class Env { @@ -58,43 +57,63 @@ export class Env { public readonly staticFilesDir: string; /** - * @internal + * Information about Kibana package (version, build number etc.). */ - constructor(readonly homeDir: string, private readonly options: EnvOptions) { - this.configDir = resolve(this.homeDir, 'config'); - this.corePluginsDir = resolve(this.homeDir, 'core_plugins'); - this.binDir = resolve(this.homeDir, 'bin'); - this.logDir = resolve(this.homeDir, 'log'); - this.staticFilesDir = resolve(this.homeDir, 'ui'); - } + public readonly packageInfo: Readonly; - public getConfigFile() { - const defaultConfigFile = this.getDefaultConfigFile(); - return this.options.config === undefined ? defaultConfigFile : this.options.config; - } + /** + * Mode Kibana currently run in (development or production). + */ + public readonly mode: Readonly; /** * @internal */ - public getLegacyKbnServer(): LegacyKbnServer | undefined { - return this.options.kbnServer; - } + public readonly legacy: EventEmitter; /** - * Gets information about Kibana package (version, build number etc.). + * Arguments provided through command line. */ - public getPackageInfo() { - return this.options.packageInfo; - } + public readonly cliArgs: Readonly>; /** - * Gets mode Kibana currently run in (development or production). + * Paths to the configuration files. */ - public getMode() { - return this.options.mode; - } + public readonly configs: ReadonlyArray; + + /** + * Indicates that this Kibana instance is run as development Node Cluster master. + */ + public readonly isDevClusterMaster: boolean; + + /** + * @internal + */ + constructor(readonly homeDir: string, options: EnvOptions) { + this.configDir = resolve(this.homeDir, 'config'); + this.corePluginsDir = resolve(this.homeDir, 'core_plugins'); + this.binDir = resolve(this.homeDir, 'bin'); + this.logDir = resolve(this.homeDir, 'log'); + this.staticFilesDir = resolve(this.homeDir, 'ui'); + + this.cliArgs = Object.freeze(options.cliArgs); + this.configs = Object.freeze(options.configs); + this.isDevClusterMaster = options.isDevClusterMaster; + + this.mode = Object.freeze({ + dev: this.cliArgs.dev, + name: this.cliArgs.dev ? 'development' : 'production', + prod: !this.cliArgs.dev, + }); + + const isKibanaDistributable = pkg.build && pkg.build.distributable === true; + this.packageInfo = Object.freeze({ + branch: pkg.branch, + buildNum: isKibanaDistributable ? pkg.build.number : Number.MAX_SAFE_INTEGER, + buildSha: isKibanaDistributable ? pkg.build.sha : 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + version: pkg.version, + }); - private getDefaultConfigFile() { - return resolve(this.configDir, 'kibana.yml'); + this.legacy = new EventEmitter(); } } diff --git a/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap b/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap new file mode 100644 index 0000000000000..3060d7b468960 --- /dev/null +++ b/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`broadcasts server and connection options to the legacy "channel" 1`] = ` +Object { + "host": "127.0.0.1", + "port": 12345, + "routes": Object { + "cors": undefined, + "payload": Object { + "maxBytes": 1024, + }, + "validate": Object { + "options": Object { + "abortEarly": false, + }, + }, + }, + "state": Object { + "strictHeader": false, + }, +} +`; diff --git a/src/core/server/http/__tests__/http_server.test.ts b/src/core/server/http/__tests__/http_server.test.ts index ed07d8220141b..7f49d153163a9 100644 --- a/src/core/server/http/__tests__/http_server.test.ts +++ b/src/core/server/http/__tests__/http_server.test.ts @@ -24,7 +24,6 @@ jest.mock('fs', () => ({ })); import Chance from 'chance'; -import http from 'http'; import supertest from 'supertest'; import { Env } from '../../config'; @@ -36,6 +35,7 @@ import { Router } from '../router'; const chance = new Chance(); +let env: Env; let server: HttpServer; let config: HttpConfig; @@ -51,7 +51,8 @@ beforeEach(() => { ssl: {}, } as HttpConfig; - server = new HttpServer(logger.get(), new Env('/kibana', getEnvOptions())); + env = new Env('/kibana', getEnvOptions()); + server = new HttpServer(logger.get(), env); }); afterEach(async () => { @@ -563,99 +564,21 @@ describe('with defined `redirectHttpFromPort`', () => { }); }); -describe('when run within legacy platform', () => { - let newPlatformProxyListenerMock: any; - beforeEach(() => { - newPlatformProxyListenerMock = { - bind: jest.fn(), - proxy: jest.fn(), - }; - - const kbnServerMock = { - newPlatformProxyListener: newPlatformProxyListenerMock, - }; - - server = new HttpServer( - logger.get(), - new Env('/kibana', getEnvOptions({ kbnServer: kbnServerMock })) - ); - - const router = new Router('/new'); - router.get({ path: '/', validate: false }, async (req, res) => { - return res.ok({ key: 'new-platform' }); - }); - - server.registerRouter(router); - - newPlatformProxyListenerMock.proxy.mockImplementation( - (req: http.IncomingMessage, res: http.ServerResponse) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ key: `legacy-platform:${req.url}` })); - } - ); - }); - - test('binds proxy listener to server.', async () => { - expect(newPlatformProxyListenerMock.bind).not.toHaveBeenCalled(); - - await server.start(config); - - expect(newPlatformProxyListenerMock.bind).toHaveBeenCalledTimes(1); - expect(newPlatformProxyListenerMock.bind).toHaveBeenCalledWith( - expect.any((http as any).Server) - ); - expect(newPlatformProxyListenerMock.bind.mock.calls[0][0]).toBe(getServerListener(server)); - }); - - test('forwards request to legacy platform if new one cannot handle it', async () => { - await server.start(config); - - await supertest(getServerListener(server)) - .get('/legacy') - .expect(200) - .then(res => { - expect(res.body).toEqual({ key: 'legacy-platform:/legacy' }); - expect(newPlatformProxyListenerMock.proxy).toHaveBeenCalledTimes(1); - expect(newPlatformProxyListenerMock.proxy).toHaveBeenCalledWith( - expect.any((http as any).IncomingMessage), - expect.any((http as any).ServerResponse) - ); - }); - }); +test('broadcasts server and connection options to the legacy "channel"', async () => { + const onConnectionListener = jest.fn(); + env.legacy.on('connection', onConnectionListener); - test('forwards request to legacy platform and rewrites base path if needed', async () => { - await server.start({ - ...config, - basePath: '/bar', - rewriteBasePath: true, - }); - - await supertest(getServerListener(server)) - .get('/legacy') - .expect(404); + expect(onConnectionListener).not.toHaveBeenCalled(); - await supertest(getServerListener(server)) - .get('/bar/legacy') - .expect(200) - .then(res => { - expect(res.body).toEqual({ key: 'legacy-platform:/legacy' }); - expect(newPlatformProxyListenerMock.proxy).toHaveBeenCalledTimes(1); - expect(newPlatformProxyListenerMock.proxy).toHaveBeenCalledWith( - expect.any((http as any).IncomingMessage), - expect.any((http as any).ServerResponse) - ); - }); + await server.start({ + ...config, + port: 12345, }); - test('do not forward request to legacy platform if new one can handle it', async () => { - await server.start(config); + expect(onConnectionListener).toHaveBeenCalledTimes(1); - await supertest(getServerListener(server)) - .get('/new/') - .expect(200) - .then(res => { - expect(res.body).toEqual({ key: 'new-platform' }); - expect(newPlatformProxyListenerMock.proxy).not.toHaveBeenCalled(); - }); - }); + const [[{ options, server: rawServer }]] = onConnectionListener.mock.calls; + expect(rawServer).toBeDefined(); + expect(rawServer).toBe((server as any).server); + expect(options).toMatchSnapshot(); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index ae02018c43545..21cde147b8ea2 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -45,7 +45,10 @@ export class HttpServer { } public async start(config: HttpConfig) { - this.server = createServer(getServerOptions(config)); + this.log.debug('starting http server'); + + const serverOptions = getServerOptions(config); + this.server = createServer(serverOptions); this.setupBasePathRewrite(this.server, config); @@ -59,32 +62,13 @@ export class HttpServer { } } - const legacyKbnServer = this.env.getLegacyKbnServer(); - if (legacyKbnServer !== undefined) { - legacyKbnServer.newPlatformProxyListener.bind(this.server.listener); - - // We register Kibana proxy middleware right before we start server to allow - // all new platform plugins register their routes, so that `legacyKbnServer` - // handles only requests that aren't handled by the new platform. - this.server.route({ - handler: ({ raw: { req, res } }, responseToolkit) => { - legacyKbnServer.newPlatformProxyListener.proxy(req, res); - return responseToolkit.abandon; - }, - method: '*', - options: { - payload: { - output: 'stream', - parse: false, - timeout: false, - // Having such a large value here will allow legacy routes to override - // maximum allowed payload size set in the core http server if needed. - maxBytes: Number.MAX_SAFE_INTEGER, - }, - }, - path: '/{p*}', - }); - } + // Notify legacy compatibility layer about HTTP(S) connection providing server + // instance with connection options so that we can properly bridge core and + // the "legacy" Kibana internally. + this.env.legacy.emit('connection', { + options: serverOptions, + server: this.server, + }); await this.server.start(); @@ -96,12 +80,13 @@ export class HttpServer { } public async stop() { - this.log.info('stopping http server'); - - if (this.server !== undefined) { - await this.server.stop(); - this.server = undefined; + if (this.server === undefined) { + return; } + + this.log.debug('stopping http server'); + await this.server.stop(); + this.server = undefined; } private setupBasePathRewrite(server: Server, config: HttpConfig) { diff --git a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap b/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap index 41d10685923df..eb58ca8cbc5fd 100644 --- a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap +++ b/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap @@ -1,3 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`correctly unbinds from the previous server. 1`] = `"Unhandled \\"error\\" event. (Error: Some error)"`; +exports[`correctly binds to the server.: proxy route options 1`] = ` +Array [ + Array [ + Object { + "handler": [Function], + "method": "*", + "options": Object { + "payload": Object { + "maxBytes": 9007199254740991, + "output": "stream", + "parse": false, + "timeout": false, + }, + }, + "path": "/{p*}", + }, + ], +] +`; diff --git a/src/core/server/legacy_compat/__tests__/legacy_kbn_server.test.ts b/src/core/server/legacy_compat/__tests__/legacy_kbn_server.test.ts deleted file mode 100644 index 72780e1882023..0000000000000 --- a/src/core/server/legacy_compat/__tests__/legacy_kbn_server.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { LegacyKbnServer } from '..'; - -test('correctly returns `newPlatformProxyListener`.', () => { - const rawKbnServer = { - newPlatform: { - proxyListener: {}, - }, - }; - - const legacyKbnServer = new LegacyKbnServer(rawKbnServer); - expect(legacyKbnServer.newPlatformProxyListener).toBe(rawKbnServer.newPlatform.proxyListener); -}); diff --git a/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts b/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts index a441b81bd171e..27db835a0ecf3 100644 --- a/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts +++ b/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts @@ -17,131 +17,74 @@ * under the License. */ -import { EventEmitter } from 'events'; -import { IncomingMessage, ServerResponse } from 'http'; - -class MockNetServer extends EventEmitter { - public address() { - return { port: 1234, family: 'test-family', address: 'test-address' }; - } - - public getConnections(callback: (error: Error | null, count: number) => void) { - callback(null, 100500); - } -} - -function mockNetServer() { - return new MockNetServer(); -} - -jest.mock('net', () => ({ - createServer: jest.fn(() => mockNetServer()), -})); - -import { createServer } from 'net'; +import { Server as HapiServer } from 'hapi-latest'; +import { Server } from 'net'; import { LegacyPlatformProxifier } from '..'; +import { Env } from '../../config'; +import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; +import { logger } from '../../logging/__mocks__'; +let server: jest.Mocked; +let mockHapiServer: jest.Mocked; let root: any; let proxifier: LegacyPlatformProxifier; beforeEach(() => { + server = { + addListener: jest.fn(), + address: jest + .fn() + .mockReturnValue({ port: 1234, family: 'test-family', address: 'test-address' }), + getConnections: jest.fn(), + } as any; + + mockHapiServer = { listener: server, route: jest.fn() } as any; + root = { - logger: { - get: jest.fn(() => ({ - debug: jest.fn(), - info: jest.fn(), - })), - }, + logger, shutdown: jest.fn(), start: jest.fn(), } as any; - proxifier = new LegacyPlatformProxifier(root); + const env = new Env('/kibana', getEnvOptions()); + proxifier = new LegacyPlatformProxifier(root, env); + env.legacy.emit('connection', { + server: mockHapiServer, + options: { someOption: 'foo', someAnotherOption: 'bar' }, + }); }); test('correctly binds to the server.', () => { - const server = createServer(); - jest.spyOn(server, 'addListener'); - proxifier.bind(server); - - expect(server.addListener).toHaveBeenCalledTimes(4); - for (const eventName of ['listening', 'error', 'clientError', 'connection']) { + expect(mockHapiServer.route.mock.calls).toMatchSnapshot('proxy route options'); + expect(server.addListener).toHaveBeenCalledTimes(6); + for (const eventName of ['clientError', 'close', 'connection', 'error', 'listening', 'upgrade']) { expect(server.addListener).toHaveBeenCalledWith(eventName, expect.any(Function)); } }); -test('correctly binds to the server and redirects its events.', () => { - const server = createServer(); - proxifier.bind(server); - - const eventsAndListeners = new Map( - ['listening', 'error', 'clientError', 'connection'].map(eventName => { - const listener = jest.fn(); - proxifier.addListener(eventName, listener); - - return [eventName, listener] as [string, () => void]; - }) - ); +test('correctly redirects server events.', () => { + for (const eventName of ['clientError', 'close', 'connection', 'error', 'listening', 'upgrade']) { + expect(server.addListener).toHaveBeenCalledWith(eventName, expect.any(Function)); - for (const [eventName, listener] of eventsAndListeners) { - expect(listener).not.toHaveBeenCalled(); + const listener = jest.fn(); + proxifier.addListener(eventName, listener); // Emit several events, to make sure that server is not being listened with `once`. - server.emit(eventName, 1, 2, 3, 4); - server.emit(eventName, 5, 6, 7, 8); + const [, serverListener] = server.addListener.mock.calls.find( + ([serverEventName]) => serverEventName === eventName + )!; + + serverListener(1, 2, 3, 4); + serverListener(5, 6, 7, 8); expect(listener).toHaveBeenCalledTimes(2); expect(listener).toHaveBeenCalledWith(1, 2, 3, 4); expect(listener).toHaveBeenCalledWith(5, 6, 7, 8); - } -}); - -test('correctly unbinds from the previous server.', () => { - const previousServer = createServer(); - proxifier.bind(previousServer); - - const currentServer = createServer(); - proxifier.bind(currentServer); - - const eventsAndListeners = new Map( - ['listening', 'error', 'clientError', 'connection'].map(eventName => { - const listener = jest.fn(); - proxifier.addListener(eventName, listener); - - return [eventName, listener] as [string, () => void]; - }) - ); - - // Any events from the previous server should not be forwarded. - for (const [eventName, listener] of eventsAndListeners) { - // `error` event is a special case in node, if `error` is emitted, but - // there is no listener for it error will be thrown. - if (eventName === 'error') { - expect(() => - previousServer.emit(eventName, new Error('Some error')) - ).toThrowErrorMatchingSnapshot(); - } else { - previousServer.emit(eventName, 1, 2, 3, 4); - } - - expect(listener).not.toHaveBeenCalled(); - } - - // Only events from the last server should be forwarded. - for (const [eventName, listener] of eventsAndListeners) { - expect(listener).not.toHaveBeenCalled(); - - currentServer.emit(eventName, 1, 2, 3, 4); - expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toHaveBeenCalledWith(1, 2, 3, 4); + proxifier.removeListener(eventName, listener); } }); test('returns `address` from the underlying server.', () => { - expect(proxifier.address()).toBeUndefined(); - - proxifier.bind(createServer()); - expect(proxifier.address()).toEqual({ address: 'test-address', family: 'test-family', @@ -168,33 +111,35 @@ test('`close` shuts down the `root`.', async () => { }); test('returns connection count from the underlying server.', () => { + server.getConnections.mockImplementation(callback => callback(null, 0)); const onGetConnectionsComplete = jest.fn(); - proxifier.getConnections(onGetConnectionsComplete); expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 0); onGetConnectionsComplete.mockReset(); - proxifier.bind(createServer()); + server.getConnections.mockImplementation(callback => callback(null, 100500)); proxifier.getConnections(onGetConnectionsComplete); expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 100500); }); -test('correctly proxies request and response objects.', () => { +test('proxy route abandons request processing and forwards it to the legacy Kibana', async () => { + const mockResponseToolkit = { response: jest.fn(), abandon: Symbol('abandon') }; + const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; + const onRequest = jest.fn(); proxifier.addListener('request', onRequest); - const request = {} as IncomingMessage; - const response = {} as ServerResponse; - proxifier.proxy(request, response); + const [[{ handler }]] = mockHapiServer.route.mock.calls; + const response = await handler(mockRequest, mockResponseToolkit); - expect(onRequest).toHaveBeenCalledTimes(1); - expect(onRequest).toHaveBeenCalledWith(request, response); + expect(response).toBe(mockResponseToolkit.abandon); + expect(mockResponseToolkit.response).not.toHaveBeenCalled(); - // Check that exactly same objects were passed as event arguments. - expect(onRequest.mock.calls[0][0]).toBe(request); - expect(onRequest.mock.calls[0][1]).toBe(response); + // Make sure request hasn't been passed to the legacy platform. + expect(onRequest).toHaveBeenCalledTimes(1); + expect(onRequest).toHaveBeenCalledWith(mockRequest.raw.req, mockRequest.raw.res); }); diff --git a/src/core/server/legacy_compat/index.ts b/src/core/server/legacy_compat/index.ts index feed0001ee216..dcc5c31fbb87d 100644 --- a/src/core/server/legacy_compat/index.ts +++ b/src/core/server/legacy_compat/index.ts @@ -19,24 +19,18 @@ import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; + /** @internal */ export { LegacyPlatformProxifier } from './legacy_platform_proxifier'; /** @internal */ export { LegacyConfigToRawConfigAdapter, LegacyConfig } from './legacy_platform_config'; -/** @internal */ -export { LegacyKbnServer } from './legacy_kbn_server'; -import { - LegacyConfig, - LegacyConfigToRawConfigAdapter, - LegacyKbnServer, - LegacyPlatformProxifier, -} from '.'; +import { LegacyConfig, LegacyConfigToRawConfigAdapter, LegacyPlatformProxifier } from '.'; import { Env } from '../config'; import { Root } from '../root'; import { BasePathProxyRoot } from '../root/base_path_proxy_root'; -function initEnvironment(rawKbnServer: any) { +function initEnvironment(rawKbnServer: any, isDevClusterMaster = false) { const config: LegacyConfig = rawKbnServer.config; const legacyConfig$ = new BehaviorSubject(config); @@ -45,12 +39,12 @@ function initEnvironment(rawKbnServer: any) { ); const env = Env.createDefault({ - kbnServer: new LegacyKbnServer(rawKbnServer), - // The defaults for the following parameters are retrieved by the legacy - // platform from the command line or from `package.json` and stored in the - // config, so we can borrow these parameters and avoid double parsing. - mode: config.get('env'), - packageInfo: config.get('pkg'), + // The core doesn't work with configs yet, everything is provided by the + // "legacy" Kibana, so we can have empty array here. + configs: [], + // `dev` is the only CLI argument we currently use. + cliArgs: { dev: config.get('env.dev') }, + isDevClusterMaster, }); return { @@ -71,12 +65,12 @@ export const injectIntoKbnServer = (rawKbnServer: any) => { rawKbnServer.newPlatform = { // Custom HTTP Listener that will be used within legacy platform by HapiJS server. - proxyListener: new LegacyPlatformProxifier(new Root(config$, env)), + proxyListener: new LegacyPlatformProxifier(new Root(config$, env), env), updateConfig, }; }; export const createBasePathProxy = (rawKbnServer: any) => { - const { env, config$ } = initEnvironment(rawKbnServer); + const { env, config$ } = initEnvironment(rawKbnServer, true /*isDevClusterMaster*/); return new BasePathProxyRoot(config$, env); }; diff --git a/src/core/server/legacy_compat/legacy_kbn_server.ts b/src/core/server/legacy_compat/legacy_kbn_server.ts deleted file mode 100644 index 4f4cd53677d3e..0000000000000 --- a/src/core/server/legacy_compat/legacy_kbn_server.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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. - */ - -/** - * Represents a wrapper around legacy `kbnServer` instance that exposes only - * a subset of `kbnServer` APIs used by the new platform. - * @internal - */ -export class LegacyKbnServer { - constructor(private readonly rawKbnServer: any) {} - - /** - * Custom HTTP Listener used by HapiJS server in the legacy platform. - */ - get newPlatformProxyListener() { - return this.rawKbnServer.newPlatform.proxyListener; - } -} diff --git a/src/core/server/legacy_compat/legacy_platform_proxifier.ts b/src/core/server/legacy_compat/legacy_platform_proxifier.ts index 8e9198799988a..8baa156266ef0 100644 --- a/src/core/server/legacy_compat/legacy_platform_proxifier.ts +++ b/src/core/server/legacy_compat/legacy_platform_proxifier.ts @@ -18,16 +18,29 @@ */ import { EventEmitter } from 'events'; -import { IncomingMessage, ServerResponse } from 'http'; import { Server } from 'net'; +import { Server as HapiServer, ServerOptions as HapiServerOptions } from 'hapi-latest'; +import { Env } from '../config'; import { Logger } from '../logging'; import { Root } from '../root'; +interface ConnectionInfo { + server: HapiServer; + options: HapiServerOptions; +} + /** * List of the server events to be forwarded to the legacy platform. */ -const ServerEventsToForward = ['listening', 'error', 'clientError', 'connection']; +const ServerEventsToForward = [ + 'clientError', + 'close', + 'connection', + 'error', + 'listening', + 'upgrade', +]; /** * Represents "proxy" between legacy and current platform. @@ -38,7 +51,7 @@ export class LegacyPlatformProxifier extends EventEmitter { private readonly log: Logger; private server?: Server; - constructor(private readonly root: Root) { + constructor(private readonly root: Root, private readonly env: Env) { super(); this.log = root.logger.get('legacy-platform-proxifier'); @@ -56,6 +69,14 @@ export class LegacyPlatformProxifier extends EventEmitter { ] as [string, (...args: any[]) => void]; }) ); + + // Once core HTTP service is ready it broadcasts the internal server it relies on + // and server options that were used to create that server so that we can properly + // bridge with the "legacy" Kibana. If server isn't run (e.g. if process is managed + // by ClusterManager or optimizer) then this event will never fire. + this.env.legacy.once('connection', (connectionInfo: ConnectionInfo) => + this.onConnection(connectionInfo) + ); } /** @@ -116,31 +137,36 @@ export class LegacyPlatformProxifier extends EventEmitter { } } - /** - * Binds Http/Https server to the LegacyPlatformProxifier. - * @param server Server to bind to. - */ - public bind(server: Server) { - const oldServer = this.server; - this.server = server; + private onConnection({ server }: ConnectionInfo) { + this.server = server.listener; for (const [eventName, eventHandler] of this.eventHandlers) { - if (oldServer !== undefined) { - oldServer.removeListener(eventName, eventHandler); - } - this.server.addListener(eventName, eventHandler); } - } - /** - * Forwards request and response objects to the legacy platform. - * This method is used whenever new platform doesn't know how to handle the request. - * @param request Native Node request object instance. - * @param response Native Node response object instance. - */ - public proxy(request: IncomingMessage, response: ServerResponse) { - this.log.debug(`Request will be handled by proxy ${request.method}:${request.url}.`); - this.emit('request', request, response); + // We register Kibana proxy middleware right before we start server to allow + // all new platform plugins register their routes, so that `legacyProxy` + // handles only requests that aren't handled by the new platform. + server.route({ + path: '/{p*}', + method: '*', + options: { + payload: { + output: 'stream', + parse: false, + timeout: false, + // Having such a large value here will allow legacy routes to override + // maximum allowed payload size set in the core http server if needed. + maxBytes: Number.MAX_SAFE_INTEGER, + }, + }, + handler: async ({ raw: { req, res } }, responseToolkit) => { + this.log.trace(`Request will be handled by proxy ${req.method}:${req.url}.`); + // Forward request and response objects to the legacy platform. This method + // is used whenever new platform doesn't know how to handle the request. + this.emit('request', req, res); + return responseToolkit.abandon; + }, + }); } } diff --git a/src/utils/package_json.js b/src/utils/package_json.ts similarity index 93% rename from src/utils/package_json.js rename to src/utils/package_json.ts index e8130345a6f9d..edacb04c6b1da 100644 --- a/src/utils/package_json.js +++ b/src/utils/package_json.ts @@ -22,5 +22,6 @@ import { dirname } from 'path'; export const pkg = { __filename: require.resolve('../../package.json'), __dirname: dirname(require.resolve('../../package.json')), - ...require('../../package.json') + // tslint:disable no-var-requires + ...require('../../package.json'), };