Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy ExpressJS requests to HapiJS. #13844

Merged
merged 41 commits into from
Oct 5, 2017

Conversation

azasypkin
Copy link
Member

Really early-draft of ExpressJS (new platform) and HapiJS (existing platform) integration.

This is supposed to handle #12520

root.logger.get('proxy'),
argv.kbnServer
));
proxy.on('platform-start', async () => await root.start());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just exploring the idea, but maybe it's not that bad if we start root only when old platform starts Hapi with our proxy as a listener. This way we reuse port that old platform supplies us via listener so we don't have to read kbnServer.settings.server.port...

interface WithConfig {
config?: string;
kbnServer?: any;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't really know where to plug this kbnServer in - env was the easiest one :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works for now. I might want to move it later, but for the PoC it's 👍

const proxy = this.env.getProxy();
if (proxy) {
proxy.bind(this.server);
port = proxy.getPort();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The real port is passed by old Kibana in setup_connection -> proxy.listen(...) so we don't have to think which port to use here, just grab what old platform gave us.

this.app.use([

// We will register body parser only for routes defined in the new platform.
/*this.app.use([
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we don't have to register body parsers "globally" and we can do that on route basis (maybe even router level). I'll check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++ I like the idea of adding them on routers instead


import { Logger } from '../../../logging';

export class Proxy extends EventEmitter {
Copy link
Member Author

@azasypkin azasypkin Sep 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not really a "proxy" - it's more like a "something-else", so just semi-randomly picked "proxy" :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProxyToLegacyPlatform, just to be clearer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

this.port = port;

this.emit('platform-start');
callback && callback();
Copy link
Member Author

@azasypkin azasypkin Sep 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we should call callback only when platform is ready, but I should think how to do that. And I haven't noticed any issues with this approach yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we removed the events and rather injected start and stop functions when creating the Proxy, we could call the callback when they finish instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea ✔️

@@ -3,6 +3,8 @@ import { map } from 'lodash';
import secureOptions from './secure_options';

export default function (kbnServer, server, config) {
const proxy = kbnServer && kbnServer.proxy;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely don't want to expose proxy property on the kbnServer, but I haven't figured out the better way yet.... Do you have any ideas?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've already got tons of things on it, so I think it's okey. We could probably rename this to be clearer. newPlatformProxyListener or something like that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

@azasypkin
Copy link
Member Author

azasypkin commented Sep 7, 2017

UPDATE:

  1. I've dug into Hapi's server.inject to understand if there is a way disable buffering of request/response and it seems impossible. Hapi uses shot under the hood and shot always reads entire request and response body into a buffer if it receives stream :/.

  2. So I started to play with custom listener instead and although it looks quite hacky, it seems to work. I've pushed updated code if you want to take a look. Now proxy stuff works correctly, response and request body is neither parsed nor buffered. We can apply body parsers just to our new-platform routes and we should be fine (I'll verifying this assumption). Also our custom listener (Proxy in my PR) will need a bit of tuning if we decide to go with something like this (like what server events to forward to old platform, do we want to pass real connection instance to old kibana etc.)

Example:

  • http://localhost:5601/api/saved_objects will be forwarded to the old platform
  • http://localhost:5601/api/saved_objects/type will stay in the new platform

/cc @kjbekkelund @spalger

Copy link
Contributor

@kimjoar kimjoar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great start. I think we should get rid of code that lets the new platform start on its own, and always have it start through old Kibana (aka we always receive kbnServer etc).

@@ -20,7 +21,7 @@ export const parseArgv = (argv: Array<string>) =>
.epilogue(args.docs)
.check(args.check(args.options)).argv;

const run = (argv: { [key: string]: any }) => {
export const run = (argv: { [key: string]: any }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we should create a separate runWithLegacyKbnServer. We probably need to explore not listening for SIGHUP, SIGINT etc here, and rather receive that through the old platform somehow. I've got a feeling we might want to receive the config from the old platform too, and have a transformLegacyConfig or something like that which transforms it into whatever we need/want in the new platform.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Or just rewrite this code to be explicit about kbnServer always being there, and getting rid of node scripts/platform entirely. Then, when we get to K7 we can add whatever we need to have the new platform start cleanly on its own)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't have strong opinion on that, having runWithLegacyKbnServer sounds reasonable to me. I'd get rid of node scritps/platform only once we have dev setup ready, I still use ts:start with scripts/platform to quickly test changes.

I've got a feeling we might want to receive the config from the old platform too, and have a transformLegacyConfig or something like that which transforms it into whatever we need/want in the new platform.

I'll explore that.

this.port = port;

this.emit('platform-start');
callback && callback();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we removed the events and rather injected start and stop functions when creating the Proxy, we could call the callback when they finish instead.

export class Proxy extends EventEmitter {
private server: Server;
private readonly eventHandlers: any;
private port: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't specified in the constructor, so should be:

private port?: number;

(Also: I'm hoping they implement microsoft/TypeScript#8476 soon)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

listening: forwardEvent.bind(this, 'listening'),
error: forwardEvent.bind(this, 'error'),
clientError: forwardEvent.bind(this, 'clientError'),
connection: forwardEvent.bind(this, 'connection')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these needed? (Might be obvious, just add a comment about it in the code)

Copy link
Member Author

@azasypkin azasypkin Sep 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


getConnections(callback: (error?: Error, count?: number) => void) {
// This method is used by `even-better` (before we start platform).
// It seems that the latest version of parent `good` doesn't use this anymore.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully we'll get rid of it soon: #13800

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that would be nice


import { Logger } from '../../../logging';

export class Proxy extends EventEmitter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProxyToLegacyPlatform, just to be clearer?

@@ -12,8 +14,17 @@ let app: any;
let port: number;

beforeEach(() => {
const log: Logger = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not something we need to fix in this PR: There is a logger mock in ../../../logging/__mocks__, not sure if that works out-of-the-box here. We need some way of sharing this across tests, though, just so we don't have to keep mocks like this up-to-date. Jest has some automatic mocking support, but I haven't looked at it yet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

@@ -3,6 +3,8 @@ import { map } from 'lodash';
import secureOptions from './secure_options';

export default function (kbnServer, server, config) {
const proxy = kbnServer && kbnServer.proxy;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've already got tons of things on it, so I think it's okey. We could probably rename this to be clearer. newPlatformProxyListener or something like that?

@azasypkin azasypkin force-pushed the issue-12520-expressjs-hapi branch 4 times, most recently from 8bd9bb2 to 7565b4e Compare September 22, 2017 11:58
@@ -52,4 +53,24 @@ const run = (argv: { [key: string]: any }) => {
}
};

export const runWithLegacyKbnServer = (kbnServer: any) => {
const root = new Root(
getLegacyConfig$(kbnServer),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kjbekkelund I've added some initial support for transforming legacy config to a new format, could you please take a look?

It also required some changes to Duration and ByteSize since in the old Kibana we use numbers for these type of values (either number of ms or bytes). I prefer the new platform approach though (strings like 1mb etc.) and would get rid of number support once we finish migration. Alternatively LegacyPlatformConfig can transform numbers to format new platform understands like newconfig.timeout: "${oldconfig.timeout}"ms, but I found it a bit clumsy :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, I'll take a look now.

Copy link
Contributor

@kimjoar kimjoar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just scanned quickly through the code and noted a couple things. Will try to run it etc now, just wanted to submit the comments so far.

@@ -31,6 +31,38 @@ describe('parsing units', () => {
});
});

describe('#constructor', () => {
test('throws if number of bytes is negative', () => {
expect(() => new ByteSizeValue(-1024)).toThrowError(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see the full error message here, maybe use toThrowErrorMatchingSnapshot instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️


export const getLegacyConfig$ = (kbnServer: any) => {
return Observable.from([
new Proxy({ [configProperty]: kbnServer.config }, configProxyHandler)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proxy isn't supported in IE11, I think(?) Or do we maybe do some Babel magic which enables it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very gooood point! Will check, totally forgot about that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, likely will have to get rid of Proxy and update ConfigService to work a bit differently :/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No way here, got rid of Proxy in favor of an additional abstraction (RawConfig) around config plain object.

@@ -52,4 +53,24 @@ const run = (argv: { [key: string]: any }) => {
}
};

export const runWithLegacyKbnServer = (kbnServer: any) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary yet, but before we merge we should create a interface LegacyKbnServer { ... } with the fields we use in ts files, just so we don't type it as any.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

() => root.shutdown()
);

// TODO: Do something to reload config.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could for example register a callback on kbnServer in this file, and then call that whenever

process.on('SIGHUP', function reloadConfig() {
triggers. Hm, maybe that would be weird as we would "go around" the old platform.

Another idea could be to create a BehaviorSubject in legacy Kibana that we expose as an Observable. We'd next on whenever the sighup triggers in the above code I linked. So here we'd only need to map over it and do the transform from legacy config stuff. Then we could remove this process.on('SIGHUP' entirely from this function.

No matter what we choose I feel it's important that the new platform can do "full config reload" from day 1.

Copy link
Member Author

@azasypkin azasypkin Sep 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, both options look the same to me, just that in the second option code will be rxjs-fied (unless I'm missing something). I'll experiment with various options and see what we can get.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Both approaches are fine with me too

private readonly log: Logger,
readonly kbnServer: any,
private readonly startPlatform: () => Promise<any>,
private readonly stopPlatform: () => Promise<any>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise<void> for both of these

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

super();

const forwardEvent = (name: string, ...args: any[]) => {
this.log.info(`Event is being forwarded: ${name}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info -> debug

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

}

proxy(request: IncomingMessage, response: OutgoingMessage) {
this.log.info(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info -> debug

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

callback && callback();
}

async close(callback: Function) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

async close(callback: () => {}) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean () => void right? If so, then ✔️

return this.server && this.server.address();
}

async listen(port: number, host: string, callback: Function) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callback: Function

to

callback: () => void

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

) {
super();

const forwardEvent = (name: string, ...args: any[]) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

niiiitpick:

const forwardEvent = (name: string) => (...args: any[]) => {
  this.log.info(`Event is being forwarded: ${name}`);
  this.emit(name, ...args);
};

then there no need to .bind below

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

cert: readFileSync(config.ssl.certificate!),
ca:
config.ssl.certificateAuthorities &&
config.ssl.certificateAuthorities!.map(caFilePath =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need the ! here, I think

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, ✔️

@@ -42,6 +45,10 @@ export class Env {
return resolve(this.pluginsDir, pluginName, 'target', 'dist');
}

getProxy(): LegacyPlatformProxifier | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getNewPlatformProxyListener?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

@@ -52,4 +53,24 @@ const run = (argv: { [key: string]: any }) => {
}
};

export const runWithLegacyKbnServer = (kbnServer: any) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, if we remove the process.on stuff below, I think we should extract this into a separate file (and a separate folder) as there's no cli related stuff left in this function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree if we don't have process.on stuff it will reside in legacy sub-folder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kjbekkelund regarding SIGINT and SIGTERM: it seems old platform doesn't handle these events (it only cleans pid file on SIGINT). I'm not sure if we should handle these events in the new platform if it's run within old one and basically acts as an advanced HTTP server, what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should listen for them specifically, but we probably want to listen for something that signals that we should shut down (just to make sure we run all the shutdown stuff correctly for later when we remove the old platform). However, I'm not sure how we indicate that in current platform.

) {
super();

const forwardEvent = (name: string) => (name: string, ...args: any[]) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove name: string, from second function here

Copy link
Member Author

@azasypkin azasypkin Sep 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yeah, good catch. I'm really surprised that it still works :) Will figure that out

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, main 'request' event is still being forwarded correctly from another method

@azasypkin azasypkin changed the title [WIP] Proxy ExpressJS requests to HapiJS. Proxy ExpressJS requests to HapiJS. Oct 2, 2017
configPath = LegacyConfigToRawConfigAdapter.flattenConfigPath(configPath);

// Explicitly disable plugins until they are supported by the legacy config.
if (!this.legacyConfig.has(configPath) && configPath.endsWith('.enabled')) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what would be the best solution here. New platform checks enabled status for every plugin (eg. pid.enabled), but old platform doesn't know these keys and throws error. So special casing and disabling such plugins was the simplest solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this (as #14288 is merged)

import { EventEmitter } from 'events';
import { IncomingMessage, ServerResponse } from 'http';

const mockConfigService = jest.fn(() => ({
Copy link
Member Author

@azasypkin azasypkin Oct 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of stuff here is just to mock Root, maybe we just need RootMock or something, but I didn't think about it too much.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like this:

diff --git a/platform/legacy/__tests__/LegacyPlatformProxifier.test.ts b/platform/legacy/__tests__/LegacyPlatformProxifier.test.ts
index 57ce148ea4..f25cfad479 100644
--- a/platform/legacy/__tests__/LegacyPlatformProxifier.test.ts
+++ b/platform/legacy/__tests__/LegacyPlatformProxifier.test.ts
@@ -1,34 +1,6 @@
 import { EventEmitter } from 'events';
 import { IncomingMessage, ServerResponse } from 'http';
 
-const mockConfigService = jest.fn(() => ({
-  atPath: jest.fn()
-}));
-jest.mock('../../config/ConfigService', () => ({
-  ConfigService: mockConfigService
-}));
-
-const mockServer = jest.fn(() => ({
-  start: jest.fn(),
-  stop: jest.fn()
-}));
-jest.mock('../../server', () => ({ Server: mockServer }));
-
-const mockLoggingService = jest.fn(() => ({
-  upgrade: jest.fn(),
-  stop: jest.fn()
-}));
-jest.mock('../../logging/LoggingService', () => ({
-  LoggingService: mockLoggingService
-}));
-
-const mockMutableLoggerFactory = jest.fn(() => ({
-  get: jest.fn(() => ({ info: jest.fn(), debug: jest.fn() }))
-}));
-jest.mock('../../logging/LoggerFactory', () => ({
-  MutableLoggerFactory: mockMutableLoggerFactory
-}));
-
 class mockNetServer extends EventEmitter {
   address() {
     return { port: 1234, family: 'test-family', address: 'test-address' };
@@ -43,7 +15,6 @@ jest.mock('net', () => ({
 }));
 
 import { BehaviorSubject } from 'rxjs';
-import { Root } from '../../root';
 import { Env, ObjectToRawConfigAdapter } from '../../config';
 import { LegacyPlatformProxifier } from '..';
 import { Server } from 'net';
@@ -54,11 +25,17 @@ beforeEach(() => {
   const env = new Env('.', {});
   const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({}));
 
-  root = new Root(config$, env, () => {});
+  root = {
+    start: jest.fn(),
+    shutdown: jest.fn(),
+    logger: {
+      get: jest.fn(() => ({
+        info: jest.fn(),
+        debug: jest.fn()
+      }))
+    } 
+  } as any;
   proxifier = new LegacyPlatformProxifier(root);
-
-  jest.spyOn(root, 'start');
-  jest.spyOn(root, 'shutdown');
 });
 
 test('correctly binds to the server.', () => {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it's much better, thanks 👍 any to the rescue :)

@@ -171,10 +173,17 @@ export default function (program) {
}

process.on('SIGHUP', function reloadConfig() {
const settings = getCurrentSettings();
const settings = transformDeprecations(getCurrentSettings());
const config = new Config(kbnServer.config.getSchema(), settings);
Copy link
Member Author

@azasypkin azasypkin Oct 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use existing schema since I need schemas registered by plugins (e.g. elasticsearch), default schema doesn't contain those.

kbnServer.server.log(['info', 'config'], 'Reloaded logging configuration due to SIGHUP.');

// If new platform config subscription is active, let's notify it with the updated config.
if (kbnServer.newPlatformConfig) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are only two legacy kibana server tests that are failing because of this PR. I haven't fixed them in this PR, but plan to do so in the follow-ups, just need more time to figure out the best way to do so.

  1. src/ui/__tests__/ui_exports_replace_injected_vars.js - test creates KibanaServer with custom plugins path that doesn't include elasticsearch and config doesn't include schema for it as well, but it's a requirement for the new platform, fix is easy.

  2. src/cli/serve/__tests__/reload_logging_config.js - this test is full of magic and fails just because of absence of tags property in new platform LogRecord, fix is also trivial, but I've deferred that to PR that will introduce legacy-json and legacy-pattern layouts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 We don't have to do them in this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@azasypkin
Copy link
Member Author

@kjbekkelund PR is now ready for your review :)

@azasypkin azasypkin force-pushed the issue-12520-expressjs-hapi branch from 7166d81 to 6385ef4 Compare October 4, 2017 11:47
Copy link
Contributor

@kimjoar kimjoar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a first pass. It's looking good, and I think we're almost there. I just wanted to send this feedback before taking another round through this to make sure I understand all of it.

@@ -1,8 +1,11 @@
import * as process from 'process';
import { resolve } from 'path';

import { LegacyPlatformProxifier } from '../legacy';

interface WithConfig {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rename to EnvOptions, and rename argv to options in the Env class?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

configPath = LegacyConfigToRawConfigAdapter.flattenConfigPath(configPath);

// Explicitly disable plugins until they are supported by the legacy config.
if (!this.legacyConfig.has(configPath) && configPath.endsWith('.enabled')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this (as #14288 is merged)

this.httpServer.close();
this.log.info('stopping http server');

if (!this.server) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: if (this.server == null)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️ (== undefined since we changed type to Server | undefined above).

import { Router } from './Router';

export class HttpServer {
private readonly app: express.Application;
private readonly httpServer: http.Server;
private server: Server | null = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Any specific reason for null? Otherwise, maybe use undefined instead? I'm okey with either, but undefined is a bit more consistent with the rest of the code base.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, there is no reason, just used null for some reason :) By the way, should we use server: Server | undefined or just server?: Server?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️ (chose private server?: Server;, let me know if you disagree)

this.server = this.initializeServer(config);

const proxyListener = this.env.getNewPlatformProxyListener();
if (proxyListener) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: proxyListener !== undefined

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

);

kbnServer.newPlatformProxyListener = new LegacyPlatformProxifier(
new Root(config$, Env.createDefault({ kbnServer }), () => {})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Maybe make the last param just default to noop in Root?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️ (I'm trying to avoid using lodash where possible, so used () => {})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

import { EventEmitter } from 'events';
import { IncomingMessage, ServerResponse } from 'http';

const mockConfigService = jest.fn(() => ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like this:

diff --git a/platform/legacy/__tests__/LegacyPlatformProxifier.test.ts b/platform/legacy/__tests__/LegacyPlatformProxifier.test.ts
index 57ce148ea4..f25cfad479 100644
--- a/platform/legacy/__tests__/LegacyPlatformProxifier.test.ts
+++ b/platform/legacy/__tests__/LegacyPlatformProxifier.test.ts
@@ -1,34 +1,6 @@
 import { EventEmitter } from 'events';
 import { IncomingMessage, ServerResponse } from 'http';
 
-const mockConfigService = jest.fn(() => ({
-  atPath: jest.fn()
-}));
-jest.mock('../../config/ConfigService', () => ({
-  ConfigService: mockConfigService
-}));
-
-const mockServer = jest.fn(() => ({
-  start: jest.fn(),
-  stop: jest.fn()
-}));
-jest.mock('../../server', () => ({ Server: mockServer }));
-
-const mockLoggingService = jest.fn(() => ({
-  upgrade: jest.fn(),
-  stop: jest.fn()
-}));
-jest.mock('../../logging/LoggingService', () => ({
-  LoggingService: mockLoggingService
-}));
-
-const mockMutableLoggerFactory = jest.fn(() => ({
-  get: jest.fn(() => ({ info: jest.fn(), debug: jest.fn() }))
-}));
-jest.mock('../../logging/LoggerFactory', () => ({
-  MutableLoggerFactory: mockMutableLoggerFactory
-}));
-
 class mockNetServer extends EventEmitter {
   address() {
     return { port: 1234, family: 'test-family', address: 'test-address' };
@@ -43,7 +15,6 @@ jest.mock('net', () => ({
 }));
 
 import { BehaviorSubject } from 'rxjs';
-import { Root } from '../../root';
 import { Env, ObjectToRawConfigAdapter } from '../../config';
 import { LegacyPlatformProxifier } from '..';
 import { Server } from 'net';
@@ -54,11 +25,17 @@ beforeEach(() => {
   const env = new Env('.', {});
   const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({}));
 
-  root = new Root(config$, env, () => {});
+  root = {
+    start: jest.fn(),
+    shutdown: jest.fn(),
+    logger: {
+      get: jest.fn(() => ({
+        info: jest.fn(),
+        debug: jest.fn()
+      }))
+    } 
+  } as any;
   proxifier = new LegacyPlatformProxifier(root);
-
-  jest.spyOn(root, 'start');
-  jest.spyOn(root, 'shutdown');
 });
 
 test('correctly binds to the server.', () => {

@@ -0,0 +1,27 @@
export class LegacyConfigMock {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a note above this with something like:

This is a partial mock of ~/src/server/config/config.js

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

readonly has: jest.Mock<boolean>;

constructor(public __rawData: Map<string, any> = new Map()) {
this.get = jest.fn(key => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if better, but could be:

diff --git a/platform/legacy/__mocks__/LegacyConfigMock.ts b/platform/legacy/__mocks__/LegacyConfigMock.ts
index 05d5614c1a..dc91c8e84a 100644
--- a/platform/legacy/__mocks__/LegacyConfigMock.ts
+++ b/platform/legacy/__mocks__/LegacyConfigMock.ts
@@ -1,10 +1,7 @@
 export class LegacyConfigMock {
-  readonly get: jest.Mock<any>;
-  readonly set: jest.Mock<void>;
-  readonly has: jest.Mock<boolean>;
+  constructor(public __rawData: Map<string, any> = new Map()) {}
 
-  constructor(public __rawData: Map<string, any> = new Map()) {
-    this.get = jest.fn(key => {
+  get = jest.fn(key => {
     // Real legacy config throws error if key is not presented in the schema.
     if (!this.__rawData.has(key)) {
       throw new TypeError(`Unknown schema key: ${key}`);
@@ -13,7 +10,7 @@ export class LegacyConfigMock {
     return this.__rawData.get(key);
   });
 
-    this.set = jest.fn((key, value) => {
+  set = jest.fn((key, value) => {
     // Real legacy config throws error if key is not presented in the schema.
     if (!this.__rawData.has(key)) {
       throw new TypeError(`Unknown schema key: ${key}`);
@@ -22,6 +19,5 @@ export class LegacyConfigMock {
     this.__rawData.set(key, value);
   });
 
-    this.has = jest.fn(key => this.__rawData.has(key));
-  }
+  has = jest.fn(key => this.__rawData.has(key));
 }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better, thanks :) We should always offload constructor code if there is a way to do so.

✔️

kbnServer.server.log(['info', 'config'], 'Reloaded logging configuration due to SIGHUP.');

// If new platform config subscription is active, let's notify it with the updated config.
if (kbnServer.newPlatformConfig) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 We don't have to do them in this PR.

@kimjoar
Copy link
Contributor

kimjoar commented Oct 4, 2017

Went through it again, and I just have the mostly nitpicks above, otherwise I'm good with merging this.

…names, do not expose RxJS Subject to legacy platform).
@azasypkin
Copy link
Member Author

Pushed commit that handles your comments @kjbekkelund, should be ready for the next round.

Copy link
Contributor

@kimjoar kimjoar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we’re good to go! 🎉🎉 LGTM

@azasypkin
Copy link
Member Author

Thanks! 🎉 Let's see if it sticks.

@azasypkin azasypkin merged commit 50231b1 into elastic:new-platform Oct 5, 2017
@azasypkin azasypkin deleted the issue-12520-expressjs-hapi branch October 5, 2017 13:06
@kimjoar kimjoar added the Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc label Nov 21, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:New Platform Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants