New Platform and Legacy platform servers integration (#39047) (#39263)

* New and Legacy platforms share http server instance.

Required to use a common security interceptor for incoming http requests

* generate docs

* remove excessive contract method

* add test for New platform compatibility

* address comments part #1

* log server running only for http server

* fix test. mutate hapi request headers for BWC with legacy

* return 503 on start

* address @eli comments

* address @joshdover comments
This commit is contained in:
Mikhail Shustov 2019-06-19 17:41:50 +02:00 committed by GitHub
parent 986b70c625
commit 2466399929
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 326 additions and 576 deletions

View file

@ -4,10 +4,10 @@
## HttpServiceStart.isListening property
Indicates if http server is listening on a port
Indicates if http server is listening on a given port
<b>Signature:</b>
```typescript
isListening: () => boolean;
isListening: (port: number) => boolean;
```

View file

@ -15,5 +15,5 @@ export interface HttpServiceStart
| Property | Type | Description |
| --- | --- | --- |
| [isListening](./kibana-plugin-server.httpservicestart.islistening.md) | <code>() =&gt; boolean</code> | Indicates if http server is listening on a port |
| [isListening](./kibana-plugin-server.httpservicestart.islistening.md) | <code>(port: number) =&gt; boolean</code> | Indicates if http server is listening on a given port |

View file

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

View file

@ -15,6 +15,5 @@ export interface InternalCoreStart
| Property | Type | Description |
| --- | --- | --- |
| [http](./kibana-plugin-server.internalcorestart.http.md) | <code>HttpServiceStart</code> | |
| [plugins](./kibana-plugin-server.internalcorestart.plugins.md) | <code>PluginsServiceStart</code> | |

View file

@ -31,7 +31,7 @@ export async function runKibanaServer({ procs, config, options }) {
...process.env,
},
cwd: installDir || KIBANA_ROOT,
wait: /Server running/,
wait: /http server running/,
});
}

View file

@ -181,7 +181,7 @@ describe('Server logging configuration', function () {
'--logging.json', 'false'
]);
watchFileUntil(logPath, /Server running at/, 2 * minute)
watchFileUntil(logPath, /http server running/, 2 * minute)
.then(() => {
// once the server is running, archive the log file and issue SIGHUP
fs.renameSync(logPath, logPathArchived);

View file

@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`returns server and connection options on start 1`] = `
Object {
"host": "127.0.0.1",
"port": 12345,
"routes": Object {
"cors": undefined,
"payload": Object {
"maxBytes": 1024,
},
"validate": Object {
"failAction": [Function],
"options": Object {
"abortEarly": false,
},
},
},
"state": Object {
"isHttpOnly": true,
"isSameSite": false,
"strictHeader": false,
},
}
`;

View file

@ -7,3 +7,24 @@ Array [
],
]
`;
exports[`spins up notReady server until started if configured with \`autoListen:true\`: 503 response 1`] = `
Object {
"body": Array [
Array [
"Kibana server is not ready yet",
],
],
"code": Array [
Array [
503,
],
],
"header": Array [
Array [
"Retry-After",
"30",
],
],
}
`;

View file

@ -62,6 +62,13 @@ test('listening after started', async () => {
await server.start();
expect(server.isListening()).toBe(true);
expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(`
Array [
Array [
"http server running",
],
]
`);
});
test('200 OK with body', async () => {
@ -579,11 +586,10 @@ test('returns server and connection options on start', async () => {
...config,
port: 12345,
};
const { options, server: innerServer } = await server.setup(configWithPort);
const { server: innerServer } = await server.setup(configWithPort);
expect(innerServer).toBeDefined();
expect(innerServer).toBe((server as any).server);
expect(options).toMatchSnapshot();
});
test('registers registerOnPostAuth interceptor several times', async () => {

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Request, Server, ServerOptions } from 'hapi';
import { Request, Server } from 'hapi';
import { Logger } from '../logging';
import { HttpConfig } from './http_config';
@ -37,7 +37,6 @@ import { BasePath } from './base_path_service';
export interface HttpServerSetup {
server: Server;
options: ServerOptions;
registerRouter: (router: Router) => void;
/**
* To define custom authentication and/or authorization mechanism for incoming requests.
@ -114,7 +113,6 @@ export class HttpServer {
this.setupBasePathRewrite(config, basePathService);
return {
options: serverOptions,
registerRouter: this.registerRouter.bind(this),
registerOnPreAuth: this.registerOnPreAuth.bind(this),
registerOnPostAuth: this.registerOnPostAuth.bind(this),
@ -156,7 +154,8 @@ export class HttpServer {
await this.server.start();
const serverPath = this.config!.rewriteBasePath || this.config!.basePath || '';
this.log.debug(`http server running at ${this.server.info.uri}${serverPath}`);
this.log.info('http server running');
this.log.debug(`http server listening on ${this.server.info.uri}${serverPath}`);
}
public async stop() {
@ -231,6 +230,9 @@ export class HttpServer {
authenticate: adoptToHapiAuthFormat(fn, (req, { state, headers }) => {
this.authState.set(req, state);
this.authHeaders.set(req, headers);
// we mutate headers only for the backward compatibility with the legacy platform.
// where some plugin read directly from headers to identify whether a user is authenticated.
Object.assign(req.headers, headers);
}),
}));
this.server.auth.strategy('session', 'login');

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Server, ServerOptions } from 'hapi';
import { Server } from 'hapi';
import { HttpService } from './http_service';
import { HttpServerSetup } from './http_server';
import { HttpServiceSetup } from './http_service';
@ -27,7 +27,6 @@ type ServiceSetupMockType = jest.Mocked<HttpServiceSetup> & {
};
const createSetupContractMock = () => {
const setupContract: ServiceSetupMockType = {
options: ({} as unknown) as ServerOptions,
// we can mock some hapi server method when we need it
server: {} as Server,
registerOnPreAuth: jest.fn(),

View file

@ -23,6 +23,7 @@ import { noop } from 'lodash';
import { BehaviorSubject } from 'rxjs';
import { HttpService, Router } from '.';
import { HttpConfigType, config } from './http_config';
import { httpServerMock } from './http_server.mocks';
import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
@ -43,6 +44,11 @@ const createConfigService = (value: Partial<HttpConfigType> = {}) => {
configService.setSchema(config.path, config.schema);
return configService;
};
const fakeHapiServer = {
start: noop,
stop: noop,
route: noop,
};
afterEach(() => {
jest.clearAllMocks();
@ -56,9 +62,9 @@ test('creates and sets up http server', async () => {
const httpServer = {
isListening: () => false,
setup: jest.fn(),
setup: jest.fn().mockReturnValue({ server: fakeHapiServer }),
start: jest.fn(),
stop: noop,
stop: jest.fn(),
};
mockHttpServer.mockImplementation(() => httpServer);
@ -69,11 +75,62 @@ test('creates and sets up http server', async () => {
expect(httpServer.setup).not.toHaveBeenCalled();
await service.setup();
expect(httpServer.setup).toHaveBeenCalledTimes(1);
expect(httpServer.setup).toHaveBeenCalled();
expect(httpServer.start).not.toHaveBeenCalled();
await service.start();
expect(httpServer.start).toHaveBeenCalledTimes(1);
expect(httpServer.start).toHaveBeenCalled();
});
test('spins up notReady server until started if configured with `autoListen:true`', async () => {
const configService = createConfigService();
const httpServer = {
isListening: () => false,
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
const notReadyHapiServer = {
start: jest.fn(),
stop: jest.fn(),
route: jest.fn(),
};
mockHttpServer
.mockImplementationOnce(() => httpServer)
.mockImplementationOnce(() => ({
setup: () => ({ server: notReadyHapiServer }),
}));
const service = new HttpService({
configService,
env: new Env('.', getEnvOptions()),
logger,
});
await service.setup();
const mockResponse: any = {
code: jest.fn().mockImplementation(() => mockResponse),
header: jest.fn().mockImplementation(() => mockResponse),
};
const mockResponseToolkit = {
response: jest.fn().mockReturnValue(mockResponse),
};
const [[{ handler }]] = notReadyHapiServer.route.mock.calls;
const response503 = await handler(httpServerMock.createRawRequest(), mockResponseToolkit);
expect(response503).toBe(mockResponse);
expect({
body: mockResponseToolkit.response.mock.calls,
code: mockResponse.code.mock.calls,
header: mockResponse.header.mock.calls,
}).toMatchSnapshot('503 response');
await service.start();
expect(httpServer.start).toBeCalledTimes(1);
expect(notReadyHapiServer.stop).toBeCalledTimes(1);
});
// this is an integration test!
@ -121,7 +178,7 @@ test('logs error if already set up', async () => {
const httpServer = {
isListening: () => true,
setup: jest.fn(),
setup: jest.fn().mockReturnValue({ server: fakeHapiServer }),
start: noop,
stop: noop,
};
@ -139,7 +196,7 @@ test('stops http server', async () => {
const httpServer = {
isListening: () => false,
setup: noop,
setup: jest.fn().mockReturnValue({ server: fakeHapiServer }),
start: noop,
stop: jest.fn(),
};
@ -157,13 +214,39 @@ test('stops http server', async () => {
expect(httpServer.stop).toHaveBeenCalledTimes(1);
});
test('stops not ready server if it is running', async () => {
const configService = createConfigService();
const mockHapiServer = {
start: jest.fn(),
stop: jest.fn(),
route: jest.fn(),
};
const httpServer = {
isListening: () => false,
setup: jest.fn().mockReturnValue({ server: mockHapiServer }),
start: noop,
stop: jest.fn(),
};
mockHttpServer.mockImplementation(() => httpServer);
const service = new HttpService({ configService, env, logger });
await service.setup();
await service.stop();
expect(mockHapiServer.stop).toHaveBeenCalledTimes(1);
});
test('register route handler', async () => {
const configService = createConfigService();
const registerRouterMock = jest.fn();
const httpServer = {
isListening: () => false,
setup: () => ({ registerRouter: registerRouterMock }),
setup: jest
.fn()
.mockReturnValue({ server: fakeHapiServer, registerRouter: registerRouterMock }),
start: noop,
stop: noop,
};
@ -181,10 +264,7 @@ test('register route handler', async () => {
test('returns http server contract on setup', async () => {
const configService = createConfigService();
const httpServer = {
server: {},
options: { someOption: true },
};
const httpServer = { server: fakeHapiServer, options: { someOption: true } };
mockHttpServer.mockImplementation(() => ({
isListening: () => false,

View file

@ -19,6 +19,7 @@
import { Observable, Subscription } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { Server } from 'hapi';
import { LoggerFactory } from '../logging';
import { CoreService } from '../../types';
@ -34,8 +35,8 @@ export interface HttpServiceSetup extends HttpServerSetup {
}
/** @public */
export interface HttpServiceStart {
/** Indicates if http server is listening on a port */
isListening: () => boolean;
/** Indicates if http server is listening on a given port */
isListening: (port: number) => boolean;
}
/** @internal */
@ -48,6 +49,7 @@ export class HttpService implements CoreService<HttpServiceSetup, HttpServiceSta
private readonly logger: LoggerFactory;
private readonly log: Logger;
private notReadyServer?: Server;
constructor(private readonly coreContext: CoreContext) {
this.logger = coreContext.logger;
@ -78,19 +80,24 @@ export class HttpService implements CoreService<HttpServiceSetup, HttpServiceSta
const httpSetup = (this.httpServer.setup(config) || {}) as HttpServiceSetup;
const setup = {
...httpSetup,
...{ createNewServer: this.createServer.bind(this) },
createNewServer: this.createServer.bind(this),
};
if (this.shouldListen(config)) {
await this.runNotReadyServer(config);
}
return setup;
}
public async start() {
const config = await this.config$.pipe(first()).toPromise();
// We shouldn't set up http service in two cases:`
// 1. If `server.autoListen` is explicitly set to `false`.
// 2. When the process is run as dev cluster master in which case cluster manager
// will fork a dedicated process where http service will be set up instead.
if (!this.coreContext.env.isDevClusterMaster && config.autoListen) {
if (this.shouldListen(config)) {
if (this.notReadyServer) {
this.log.debug('stopping NotReady server');
await this.notReadyServer.stop();
this.notReadyServer = undefined;
}
// If a redirect port is specified, we start an HTTP server at this port and
// redirect all requests to the SSL port.
if (config.ssl.enabled && config.ssl.redirectHttpFromPort !== undefined) {
@ -102,7 +109,7 @@ export class HttpService implements CoreService<HttpServiceSetup, HttpServiceSta
}
return {
isListening: (port = 0) => {
isListening: (port: number = 0) => {
const server = this.secondaryServers.get(port);
if (server) return server.isListening();
return this.httpServer.isListening();
@ -110,6 +117,18 @@ export class HttpService implements CoreService<HttpServiceSetup, HttpServiceSta
};
}
/**
* Indicates if http server has configured to start listening on a configured port.
* We shouldn't start http service in two cases:
* 1. If `server.autoListen` is explicitly set to `false`.
* 2. When the process is run as dev cluster master in which case cluster manager
* will fork a dedicated process where http service will be set up instead.
* @internal
* */
private shouldListen(config: HttpConfig) {
return !this.coreContext.env.isDevClusterMaster && config.autoListen;
}
private async createServer(cfg: Partial<HttpConfig>) {
const { port } = cfg;
const config = await this.config$.pipe(first()).toPromise();
@ -145,9 +164,37 @@ export class HttpService implements CoreService<HttpServiceSetup, HttpServiceSta
this.configSubscription.unsubscribe();
this.configSubscription = undefined;
if (this.notReadyServer) {
await this.notReadyServer.stop();
}
await this.httpServer.stop();
await this.httpsRedirectServer.stop();
await Promise.all([...this.secondaryServers.values()].map(s => s.stop()));
this.secondaryServers.clear();
}
private async runNotReadyServer(config: HttpConfig) {
this.log.debug('starting NotReady server');
const httpServer = new HttpServer(this.log);
const { server } = await httpServer.setup(config);
this.notReadyServer = server;
// use hapi server while Kibana ResponseFactory doesn't allow specifying custom headers
// https://github.com/elastic/kibana/issues/33779
this.notReadyServer.route({
path: '/{p*}',
method: '*',
handler: (req, responseToolkit) => {
this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url}.`);
// If server is not ready yet, because plugins or core can perform
// long running tasks (build assets, saved objects migrations etc.)
// we should let client know that and ask to retry after 30 seconds.
return responseToolkit
.response('Kibana server is not ready yet')
.code(503)
.header('Retry-After', '30');
},
});
await this.notReadyServer.start();
}
}

View file

@ -83,7 +83,7 @@ describe('http service', () => {
.get(root, legacyUrl)
.expect(200, 'ok from legacy server');
expect(response.header['set-cookie']).toBe(undefined);
expect(response.header['set-cookie']).toHaveLength(1);
});
it('passes authHeaders as request headers to the legacy platform', async () => {
@ -152,7 +152,7 @@ describe('http service', () => {
expect(response.body.state).toEqual(user);
expect(response.body.status).toEqual('authenticated');
expect(response.header['set-cookie']).toBe(undefined);
expect(response.header['set-cookie']).toHaveLength(1);
});
it('rewrites authorization header via authHeaders to make a request to Elasticsearch', async () => {
@ -164,7 +164,7 @@ describe('http service', () => {
return t.authenticated({ headers: authHeaders });
}, cookieOptions);
const router = new Router('');
const router = new Router('/new-platform');
router.get({ path: '/', validate: false }, async (req, res) => {
const client = await elasticsearch.dataClient$.pipe(first()).toPromise();
client.asScoped(req);
@ -174,7 +174,7 @@ describe('http service', () => {
await root.start();
await kbnTestServer.request.get(root, '/').expect(200);
await kbnTestServer.request.get(root, '/new-platform/').expect(200);
expect(clusterClientMock).toBeCalledTimes(1);
const [firstCall] = clusterClientMock.mock.calls;
const [, , headers] = firstCall;
@ -186,7 +186,7 @@ describe('http service', () => {
const { http, elasticsearch } = await root.setup();
const { registerRouter } = http;
const router = new Router('');
const router = new Router('/new-platform');
router.get({ path: '/', validate: false }, async (req, res) => {
const client = await elasticsearch.dataClient$.pipe(first()).toPromise();
client.asScoped(req);
@ -197,7 +197,7 @@ describe('http service', () => {
await root.start();
await kbnTestServer.request
.get(root, '/')
.get(root, '/new-platform/')
.set('Authorization', authorizationHeader)
.expect(200);
@ -218,7 +218,7 @@ describe('http service', () => {
afterEach(async () => await root.shutdown());
it('supports passing request through to the route handler', async () => {
const router = new Router('');
const router = new Router('/new-platform');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
const { http } = await root.setup();
@ -230,7 +230,7 @@ describe('http service', () => {
http.registerRouter(router);
await root.start();
await kbnTestServer.request.get(root, '/').expect(200, { content: 'ok' });
await kbnTestServer.request.get(root, '/new-platform/').expect(200, { content: 'ok' });
});
it('supports redirecting to configured url', async () => {
@ -239,7 +239,7 @@ describe('http service', () => {
http.registerOnPostAuth(async (req, t) => t.redirected(redirectTo));
await root.start();
const response = await kbnTestServer.request.get(root, '/').expect(302);
const response = await kbnTestServer.request.get(root, '/new-platform/').expect(302);
expect(response.header.location).toBe(redirectTo);
});
@ -251,7 +251,7 @@ describe('http service', () => {
await root.start();
await kbnTestServer.request
.get(root, '/')
.get(root, '/new-platform/')
.expect(400, { statusCode: 400, error: 'Bad Request', message: 'unexpected error' });
});
@ -262,7 +262,7 @@ describe('http service', () => {
});
await root.start();
await kbnTestServer.request.get(root, '/').expect({
await kbnTestServer.request.get(root, '/new-platform/').expect({
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error occurred',
@ -283,7 +283,7 @@ describe('http service', () => {
}
return t.next();
});
const router = new Router('');
const router = new Router('/new-platform');
router.get({ path: '/', validate: false }, async (req, res) =>
// @ts-ignore. don't complain customField is not defined on Request type
res.ok({ customField: String(req.customField) })
@ -291,7 +291,9 @@ describe('http service', () => {
http.registerRouter(router);
await root.start();
await kbnTestServer.request.get(root, '/').expect(200, { customField: 'undefined' });
await kbnTestServer.request
.get(root, '/new-platform/')
.expect(200, { customField: 'undefined' });
});
});
@ -305,10 +307,10 @@ describe('http service', () => {
it('supports Url change on the flight', async () => {
const { http } = await root.setup();
http.registerOnPreAuth((req, t) => {
return t.redirected('/new-url', { forward: true });
return t.redirected('/new-platform/new-url', { forward: true });
});
const router = new Router('/');
const router = new Router('/new-platform');
router.get({ path: '/new-url', validate: false }, async (req, res) =>
res.ok({ key: 'new-url-reached' })
);

View file

@ -18,9 +18,9 @@
*/
import { httpServiceMock } from './http/http_service.mock';
export const httpService = httpServiceMock.create();
export const mockHttpService = httpServiceMock.create();
jest.doMock('./http/http_service', () => ({
HttpService: jest.fn(() => httpService),
HttpService: jest.fn(() => mockHttpService),
}));
import { pluginServiceMock } from './plugins/plugins_service.mock';
@ -30,9 +30,9 @@ jest.doMock('./plugins/plugins_service', () => ({
}));
import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
export const elasticsearchService = elasticsearchServiceMock.create();
export const mockElasticsearchService = elasticsearchServiceMock.create();
jest.doMock('./elasticsearch/elasticsearch_service', () => ({
ElasticsearchService: jest.fn(() => elasticsearchService),
ElasticsearchService: jest.fn(() => mockElasticsearchService),
}));
export const mockLegacyService = { setup: jest.fn(), start: jest.fn(), stop: jest.fn() };
@ -41,7 +41,7 @@ jest.mock('./legacy/legacy_service', () => ({
}));
import { configServiceMock } from './config/config_service.mock';
export const configService = configServiceMock.create();
export const mockConfigService = configServiceMock.create();
jest.doMock('./config/config_service', () => ({
ConfigService: jest.fn(() => configService),
ConfigService: jest.fn(() => mockConfigService),
}));

View file

@ -139,7 +139,6 @@ export interface InternalCoreSetup {
* @public
*/
export interface InternalCoreStart {
http: HttpServiceStart;
plugins: PluginsServiceStart;
}

View file

@ -46,27 +46,6 @@ Array [
exports[`once LegacyService is set up with connection info creates legacy kbnServer and closes it if \`listen\` fails. 1`] = `"something failed"`;
exports[`once LegacyService is set up with connection info proxy route responds with \`503\` if \`kbnServer\` is not ready yet.: 503 response 1`] = `
Object {
"body": Array [
Array [
"Kibana server is not ready yet",
],
],
"code": Array [
Array [
503,
],
],
"header": Array [
Array [
"Retry-After",
"30",
],
],
}
`;
exports[`once LegacyService is set up with connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = `
Array [
Array [
@ -79,26 +58,6 @@ Array [
]
`;
exports[`once LegacyService is set up with connection info register proxy route.: proxy route options 1`] = `
Array [
Array [
Object {
"handler": [Function],
"method": "*",
"options": Object {
"payload": Object {
"maxBytes": 9007199254740991,
"output": "stream",
"parse": false,
"timeout": false,
},
},
"path": "/{p*}",
},
],
]
`;
exports[`once LegacyService is set up with connection info throws if fails to retrieve initial config. 1`] = `"something failed"`;
exports[`once LegacyService is set up without connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = `

View file

@ -0,0 +1,77 @@
/*
* 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 { Router } from '../../http/';
import * as kbnTestServer from '../../../../test_utils/kbn_server';
describe('legacy service', () => {
describe('http server', () => {
let root: ReturnType<typeof kbnTestServer.createRoot>;
beforeEach(() => {
root = kbnTestServer.createRoot();
}, 30000);
afterEach(async () => await root.shutdown());
it("handles http request in Legacy platform if New platform doesn't handle it", async () => {
const rootUrl = '/route';
const router = new Router(rootUrl);
router.get({ path: '/new-platform', validate: false }, (req, res) =>
res.ok({ content: 'from-new-platform' })
);
const { http } = await root.setup();
http.registerRouter(router);
await root.start();
const legacyPlatformUrl = `${rootUrl}/legacy-platform`;
const kbnServer = kbnTestServer.getKbnServer(root);
kbnServer.server.route({
method: 'GET',
path: legacyPlatformUrl,
handler: () => 'ok from legacy server',
});
await kbnTestServer.request
.get(root, '/route/new-platform')
.expect(200, { content: 'from-new-platform' });
await kbnTestServer.request.get(root, legacyPlatformUrl).expect(200, 'ok from legacy server');
});
it('throws error if Legacy and New platforms register handler for the same route', async () => {
const rootUrl = '/route';
const router = new Router(rootUrl);
router.get({ path: '', validate: false }, (req, res) =>
res.ok({ content: 'from-new-platform' })
);
const { http } = await root.setup();
http.registerRouter(router);
await root.start();
const kbnServer = kbnTestServer.getKbnServer(root);
expect(() =>
kbnServer.server.route({
method: 'GET',
path: rootUrl,
handler: () => 'ok from legacy server',
})
).toThrowErrorMatchingInlineSnapshot(`"New route /route conflicts with existing /route"`);
});
});
});

View file

@ -1,97 +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 { Server } from 'net';
import { LegacyPlatformProxy } from './legacy_platform_proxy';
let server: jest.Mocked<Server>;
let proxy: LegacyPlatformProxy;
beforeEach(() => {
server = {
addListener: jest.fn(),
address: jest
.fn()
.mockReturnValue({ port: 1234, family: 'test-family', address: 'test-address' }),
getConnections: jest.fn(),
} as any;
proxy = new LegacyPlatformProxy({ debug: jest.fn() } as any, server);
});
test('correctly redirects server events.', () => {
for (const eventName of ['clientError', 'close', 'connection', 'error', 'listening', 'upgrade']) {
expect(server.addListener).toHaveBeenCalledWith(eventName, expect.any(Function));
const listener = jest.fn();
proxy.addListener(eventName, listener);
// Emit several events, to make sure that server is not being listened with `once`.
const [, serverListener] = server.addListener.mock.calls.find(
([serverEventName]) => serverEventName === eventName
)!;
(serverListener as jest.Mock)(1, 2, 3, 4);
(serverListener as jest.Mock)(5, 6, 7, 8);
expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenCalledWith(1, 2, 3, 4);
proxy.removeListener(eventName, listener);
}
});
test('returns `address` from the underlying server.', () => {
expect(proxy.address()).toEqual({
address: 'test-address',
family: 'test-family',
port: 1234,
});
});
test('`listen` calls callback immediately.', async () => {
const onListenComplete = jest.fn();
await proxy.listen(1234, 'host-1', onListenComplete);
expect(onListenComplete).toHaveBeenCalledTimes(1);
});
test('`close` calls callback immediately.', async () => {
const onCloseComplete = jest.fn();
await proxy.close(onCloseComplete);
expect(onCloseComplete).toHaveBeenCalledTimes(1);
});
test('returns connection count from the underlying server.', () => {
server.getConnections.mockImplementation(callback => callback(null, 0));
const onGetConnectionsComplete = jest.fn();
proxy.getConnections(onGetConnectionsComplete);
expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1);
expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 0);
onGetConnectionsComplete.mockReset();
server.getConnections.mockImplementation(callback => callback(null, 100500));
proxy.getConnections(onGetConnectionsComplete);
expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1);
expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 100500);
});

View file

@ -1,105 +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 { EventEmitter } from 'events';
import { Server } from 'net';
import { Logger } from '../logging';
/**
* List of the server events to be forwarded to the legacy platform.
*/
const ServerEventsToForward = [
'clientError',
'close',
'connection',
'error',
'listening',
'upgrade',
];
/**
* Represents "proxy" between legacy and current platform.
* @internal
*/
export class LegacyPlatformProxy extends EventEmitter {
private readonly eventHandlers: Map<string, (...args: any[]) => void>;
constructor(private readonly log: Logger, private readonly server: Server) {
super();
// HapiJS expects that the following events will be generated by `listener`, see:
// https://github.com/hapijs/hapi/blob/v14.2.0/lib/connection.js.
this.eventHandlers = new Map(
ServerEventsToForward.map(eventName => {
return [
eventName,
(...args: any[]) => {
this.log.debug(`Event is being forwarded: ${eventName}`);
this.emit(eventName, ...args);
},
] as [string, (...args: any[]) => void];
})
);
for (const [eventName, eventHandler] of this.eventHandlers) {
this.server.addListener(eventName, eventHandler);
}
}
/**
* Neither new nor legacy platform should use this method directly.
*/
public address() {
this.log.debug('"address" has been called.');
return this.server.address();
}
/**
* Neither new nor legacy platform should use this method directly.
*/
public listen(port: number, host: string, callback?: (error?: Error) => void) {
this.log.debug(`"listen" has been called (${host}:${port}).`);
if (callback !== undefined) {
callback();
}
}
/**
* Neither new nor legacy platform should use this method directly.
*/
public close(callback?: (error?: Error) => void) {
this.log.debug('"close" has been called.');
if (callback !== undefined) {
callback();
}
}
/**
* Neither new nor legacy platform should use this method directly.
*/
public getConnections(callback: (error: Error | null, 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.
this.server.getConnections(callback);
}
}

View file

@ -17,13 +17,11 @@
* under the License.
*/
import { BehaviorSubject, Subject, throwError } from 'rxjs';
import { BehaviorSubject, throwError } from 'rxjs';
jest.mock('./legacy_platform_proxy');
jest.mock('../../../legacy/server/kbn_server');
jest.mock('../../../cli/cluster/cluster_manager');
import { first } from 'rxjs/operators';
import { LegacyService } from '.';
// @ts-ignore: implicit any for JS file
import MockClusterManager from '../../../cli/cluster/cluster_manager';
@ -36,10 +34,8 @@ import { HttpServiceStart, BasePathProxyServer } from '../http';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins';
import { PluginsServiceSetup, PluginsServiceStart } from '../plugins/plugins_service';
import { LegacyPlatformProxy } from './legacy_platform_proxy';
const MockKbnServer: jest.Mock<KbnServer> = KbnServer as any;
const MockLegacyPlatformProxy: jest.Mock<LegacyPlatformProxy> = LegacyPlatformProxy as any;
let env: Env;
let config$: BehaviorSubject<Config>;
@ -73,8 +69,6 @@ beforeEach(() => {
core: {
elasticsearch: { legacy: {} } as any,
http: {
options: { someOption: 'foo', someAnotherOption: 'bar' },
server: { listener: { addListener: jest.fn() }, route: jest.fn() },
auth: {
getAuthHeaders: () => undefined,
},
@ -116,72 +110,6 @@ afterEach(() => {
});
describe('once LegacyService is set up with connection info', () => {
test('register proxy route.', async () => {
const legacyService = new LegacyService({ env, logger, configService: configService as any });
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
expect(setupDeps.core.http.server.route.mock.calls).toMatchSnapshot('proxy route options');
});
test('proxy route responds with `503` if `kbnServer` is not ready yet.', async () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
const legacyService = new LegacyService({ env, logger, configService: configService as any });
const kbnServerListen$ = new Subject();
MockKbnServer.prototype.listen = jest.fn(() => {
kbnServerListen$.next();
return kbnServerListen$.toPromise();
});
// Wait until listen is called and proxy route is registered, but don't allow
// listen to complete and make kbnServer available.
await legacyService.setup(setupDeps);
const legacySetupPromise = legacyService.start(startDeps);
await kbnServerListen$.pipe(first()).toPromise();
const mockResponse: any = {
code: jest.fn().mockImplementation(() => mockResponse),
header: jest.fn().mockImplementation(() => mockResponse),
};
const mockResponseToolkit = {
response: jest.fn().mockReturnValue(mockResponse),
abandon: Symbol('abandon'),
};
const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } };
const [[{ handler }]] = setupDeps.core.http.server.route.mock.calls;
const response503 = await handler(mockRequest, mockResponseToolkit);
expect(response503).toBe(mockResponse);
expect({
body: mockResponseToolkit.response.mock.calls,
code: mockResponse.code.mock.calls,
header: mockResponse.header.mock.calls,
}).toMatchSnapshot('503 response');
// Make sure request hasn't been passed to the legacy platform.
const [mockedLegacyPlatformProxy] = MockLegacyPlatformProxy.mock.instances;
expect(mockedLegacyPlatformProxy.emit).not.toHaveBeenCalled();
// Now wait until kibana is ready and try to request once again.
kbnServerListen$.complete();
await legacySetupPromise;
mockResponseToolkit.response.mockClear();
const responseProxy = await handler(mockRequest, mockResponseToolkit);
expect(responseProxy).toBe(mockResponseToolkit.abandon);
expect(mockResponseToolkit.response).not.toHaveBeenCalled();
// Make sure request has been passed to the legacy platform.
expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledTimes(1);
expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledWith(
'request',
mockRequest.raw.req,
mockRequest.raw.res
);
});
test('creates legacy kbnServer and calls `listen`.', async () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
const legacyService = new LegacyService({ env, logger, configService: configService as any });
@ -195,11 +123,6 @@ describe('once LegacyService is set up with connection info', () => {
{
setupDeps,
startDeps,
serverOptions: {
listener: expect.any(LegacyPlatformProxy),
someAnotherOption: 'bar',
someOption: 'foo',
},
handledConfigPaths: ['foo.bar'],
logger,
}
@ -223,11 +146,6 @@ describe('once LegacyService is set up with connection info', () => {
{
setupDeps,
startDeps,
serverOptions: {
listener: expect.any(LegacyPlatformProxy),
someAnotherOption: 'bar',
someOption: 'foo',
},
handledConfigPaths: ['foo.bar'],
logger,
}
@ -312,59 +230,24 @@ describe('once LegacyService is set up with connection info', () => {
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
expect(loggingServiceMock.collect(logger).error).toEqual([[configError]]);
});
test('proxy route abandons request processing and forwards it to the legacy Kibana', async () => {
const legacyService = new LegacyService({ env, logger, configService: configService as any });
const mockResponseToolkit = { response: jest.fn(), abandon: Symbol('abandon') };
const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } };
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
const [[{ handler }]] = setupDeps.core.http.server.route.mock.calls;
const response = await handler(mockRequest, mockResponseToolkit);
expect(response).toBe(mockResponseToolkit.abandon);
expect(mockResponseToolkit.response).not.toHaveBeenCalled();
// Make sure request has been passed to the legacy platform.
const [mockedLegacyPlatformProxy] = MockLegacyPlatformProxy.mock.instances;
expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledTimes(1);
expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledWith(
'request',
mockRequest.raw.req,
mockRequest.raw.res
);
});
});
describe('once LegacyService is set up without connection info', () => {
const disabledHttpStartDeps = {
core: {
http: {
isListening: () => false,
},
plugins: { contracts: new Map() },
},
plugins: {},
};
let legacyService: LegacyService;
beforeEach(async () => {
legacyService = new LegacyService({ env, logger, configService: configService as any });
await legacyService.setup(setupDeps);
await legacyService.start(disabledHttpStartDeps);
await legacyService.start(startDeps);
});
test('creates legacy kbnServer with `autoListen: false`.', () => {
expect(setupDeps.core.http.server.route).not.toHaveBeenCalled();
expect(MockKbnServer).toHaveBeenCalledTimes(1);
expect(MockKbnServer).toHaveBeenCalledWith(
{ server: { autoListen: true } },
{
setupDeps,
startDeps: disabledHttpStartDeps,
serverOptions: { autoListen: false },
startDeps,
handledConfigPaths: ['foo.bar'],
logger,
}
@ -405,15 +288,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
});
await devClusterLegacyService.setup(setupDeps);
await devClusterLegacyService.start({
core: {
http: {
isListening: () => false,
},
plugins: { contracts: new Map() },
},
plugins: {},
});
await devClusterLegacyService.start(startDeps);
expect(MockClusterManager.create.mock.calls).toMatchSnapshot(
'cluster manager without base path proxy'
@ -433,15 +308,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
});
await devClusterLegacyService.setup(setupDeps);
await devClusterLegacyService.start({
core: {
http: {
isListening: () => false,
},
plugins: { contracts: new Map() },
},
plugins: {},
});
await devClusterLegacyService.start(startDeps);
expect(MockClusterManager.create).toBeCalledTimes(1);

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { Server as HapiServer } from 'hapi';
import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs';
import { first, map, mergeMap, publishReplay, tap } from 'rxjs/operators';
import { CoreService } from '../../types';
@ -27,7 +26,6 @@ import { CoreContext } from '../core_context';
import { DevConfig, DevConfigType } from '../dev';
import { BasePathProxyServer, HttpConfig, HttpConfigType } from '../http';
import { Logger } from '../logging';
import { LegacyPlatformProxy } from './legacy_platform_proxy';
interface LegacyKbnServer {
applyLoggingConfiguration: (settings: Readonly<Record<string, any>>) => void;
@ -149,17 +147,6 @@ export class LegacyService implements CoreService {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const KbnServer = require('../../../legacy/server/kbn_server');
const kbnServer: LegacyKbnServer = new KbnServer(getLegacyRawConfig(config), {
// If core HTTP service is run we'll receive internal server reference and
// 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 we won't have that info,
// so we can't start "legacy" server either.
serverOptions: startDeps.core.http.isListening()
? {
...setupDeps.core.http.options,
listener: this.setupProxyListener(setupDeps.core.http.server),
}
: { autoListen: false },
handledConfigPaths: await this.coreContext.configService.getUsedPaths(),
setupDeps,
startDeps,
@ -188,62 +175,4 @@ export class LegacyService implements CoreService {
return kbnServer;
}
private setupProxyListener(server: HapiServer) {
const { setupDeps } = this;
if (!setupDeps) {
throw new Error('Legacy service is not setup yet.');
}
const legacyProxy = new LegacyPlatformProxy(
this.coreContext.logger.get('legacy-proxy'),
server.listener
);
// 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 (request, responseToolkit) => {
const { req, res } = request.raw;
const authHeaders = setupDeps.core.http.auth.getAuthHeaders(request);
if (authHeaders) {
// some plugins in Legacy relay on headers.authorization presence
req.headers = Object.assign(req.headers, authHeaders);
}
if (this.kbnServer === undefined) {
this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url}.`);
// If legacy server is not ready yet (e.g. it's still in optimization phase),
// we should let client know that and ask to retry after 30 seconds.
return responseToolkit
.response('Kibana server is not ready yet')
.code(503)
.header('Retry-After', '30');
}
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.
legacyProxy.emit('request', req, res);
return responseToolkit.abandon;
},
});
return legacyProxy;
}
}

View file

@ -16,7 +16,6 @@ import { ResponseObject } from 'hapi';
import { ResponseToolkit } from 'hapi';
import { Schema } from '@kbn/config-schema';
import { Server } from 'hapi';
import { ServerOptions } from 'hapi';
import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { Url } from 'url';
@ -160,7 +159,7 @@ export interface HttpServiceSetup extends HttpServerSetup {
// @public (undocumented)
export interface HttpServiceStart {
isListening: () => boolean;
isListening: (port: number) => boolean;
}
// @internal (undocumented)
@ -175,8 +174,6 @@ export interface InternalCoreSetup {
// @public (undocumented)
export interface InternalCoreStart {
// (undocumented)
http: HttpServiceStart;
// (undocumented)
plugins: PluginsServiceStart;
}

View file

@ -18,11 +18,11 @@
*/
import {
elasticsearchService,
httpService,
mockElasticsearchService,
mockHttpService,
mockLegacyService,
mockPluginsService,
configService,
mockConfigService,
} from './index.test.mocks';
import { BehaviorSubject } from 'rxjs';
@ -36,7 +36,7 @@ const env = new Env('.', getEnvOptions());
const logger = loggingServiceMock.create();
beforeEach(() => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
});
afterEach(() => {
@ -47,15 +47,15 @@ const config$ = new BehaviorSubject<Config>(new ObjectToConfigAdapter({}));
test('sets up services on "setup"', async () => {
const server = new Server(config$, env, logger);
expect(httpService.setup).not.toHaveBeenCalled();
expect(elasticsearchService.setup).not.toHaveBeenCalled();
expect(mockHttpService.setup).not.toHaveBeenCalled();
expect(mockElasticsearchService.setup).not.toHaveBeenCalled();
expect(mockPluginsService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.setup).not.toHaveBeenCalled();
await server.setup();
expect(httpService.setup).toHaveBeenCalledTimes(1);
expect(elasticsearchService.setup).toHaveBeenCalledTimes(1);
expect(mockHttpService.setup).toHaveBeenCalledTimes(1);
expect(mockElasticsearchService.setup).toHaveBeenCalledTimes(1);
expect(mockPluginsService.setup).toHaveBeenCalledTimes(1);
expect(mockLegacyService.setup).toHaveBeenCalledTimes(1);
});
@ -63,21 +63,21 @@ test('sets up services on "setup"', async () => {
test('runs services on "start"', async () => {
const server = new Server(config$, env, logger);
expect(httpService.setup).not.toHaveBeenCalled();
expect(mockHttpService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.start).not.toHaveBeenCalled();
await server.setup();
expect(httpService.start).not.toHaveBeenCalled();
expect(mockHttpService.start).not.toHaveBeenCalled();
expect(mockLegacyService.start).not.toHaveBeenCalled();
await server.start();
expect(httpService.start).toHaveBeenCalledTimes(1);
expect(mockHttpService.start).toHaveBeenCalledTimes(1);
expect(mockLegacyService.start).toHaveBeenCalledTimes(1);
});
test('does not fail on "setup" if there are unused paths detected', async () => {
configService.getUnusedPaths.mockResolvedValue(['some.path', 'another.path']);
mockConfigService.getUnusedPaths.mockResolvedValue(['some.path', 'another.path']);
const server = new Server(config$, env, logger);
@ -89,29 +89,29 @@ test('stops services on "stop"', async () => {
await server.setup();
expect(httpService.stop).not.toHaveBeenCalled();
expect(elasticsearchService.stop).not.toHaveBeenCalled();
expect(mockHttpService.stop).not.toHaveBeenCalled();
expect(mockElasticsearchService.stop).not.toHaveBeenCalled();
expect(mockPluginsService.stop).not.toHaveBeenCalled();
expect(mockLegacyService.stop).not.toHaveBeenCalled();
await server.stop();
expect(httpService.stop).toHaveBeenCalledTimes(1);
expect(elasticsearchService.stop).toHaveBeenCalledTimes(1);
expect(mockHttpService.stop).toHaveBeenCalledTimes(1);
expect(mockElasticsearchService.stop).toHaveBeenCalledTimes(1);
expect(mockPluginsService.stop).toHaveBeenCalledTimes(1);
expect(mockLegacyService.stop).toHaveBeenCalledTimes(1);
});
test(`doesn't setup core services if config validation fails`, async () => {
configService.setSchema.mockImplementation(() => {
mockConfigService.setSchema.mockImplementation(() => {
throw new Error('invalid config');
});
const server = new Server(config$, env, logger);
await expect(server.setupConfigSchemas()).rejects.toThrowErrorMatchingInlineSnapshot(
`"invalid config"`
);
expect(httpService.setup).not.toHaveBeenCalled();
expect(elasticsearchService.setup).not.toHaveBeenCalled();
expect(mockHttpService.setup).not.toHaveBeenCalled();
expect(mockElasticsearchService.setup).not.toHaveBeenCalled();
expect(mockPluginsService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.setup).not.toHaveBeenCalled();
});

View file

@ -85,11 +85,9 @@ export class Server {
}
public async start() {
const httpStart = await this.http.start();
const pluginsStart = await this.plugins.start({});
const coreStart = {
http: httpStart,
plugins: pluginsStart,
};
@ -98,6 +96,7 @@ export class Server {
plugins: mapToObject(pluginsStart.contracts),
});
await this.http.start();
return coreStart;
}

View file

@ -21,14 +21,14 @@ import { format } from 'url';
import { resolve } from 'path';
import _ from 'lodash';
import Boom from 'boom';
import Hapi from 'hapi';
import { setupVersionCheck } from './version_check';
import { registerHapiPlugins } from './register_hapi_plugins';
import { setupBasePathProvider } from './setup_base_path_provider';
import { setupXsrf } from './xsrf';
export default async function (kbnServer, server, config) {
kbnServer.server = new Hapi.Server(kbnServer.newPlatform.params.serverOptions);
kbnServer.server = kbnServer.newPlatform.setup.core.http.server;
server = kbnServer.server;
setupBasePathProvider(kbnServer);

View file

@ -103,7 +103,6 @@ export default class KbnServer {
};
stop: null;
params: {
serverOptions: ElasticsearchServiceSetup;
handledConfigPaths: Unpromise<ReturnType<ConfigService['getUsedPaths']>>;
};
};

View file

@ -55,7 +55,7 @@ export default class KbnServer {
this.rootDir = rootDir;
this.settings = settings || {};
const { setupDeps, startDeps, serverOptions, handledConfigPaths, logger } = core;
const { setupDeps, startDeps, handledConfigPaths, logger } = core;
this.newPlatform = {
coreContext: {
logger,
@ -64,7 +64,6 @@ export default class KbnServer {
start: startDeps,
stop: null,
params: {
serverOptions,
handledConfigPaths,
},
};

View file

@ -29,7 +29,7 @@ module.exports = function (grunt) {
return {
options: {
wait: false,
ready: /Server running/,
ready: /http server running/,
quiet: false,
failOnError: false
},

View file

@ -23,6 +23,13 @@ export default function ({ getService }) {
expect(response.headers.location).to.be('/login?next=%2Fabc%2Fxyz');
});
it('should redirect non-AJAX New platform requests to the login page if not authenticated', async () => {
const response = await supertest.get('/core/')
.expect(302);
expect(response.headers.location).to.be('/login?next=%2Fcore%2F');
});
it('should reject API requests if client is not authenticated', async () => {
await supertest
.get('/api/security/v1/me')