* extract http_tools to package
* fix readme
* start moving stuff
* cleaning up `isDevCliParent`
* choose bootstrap script
* fix bootstrap script logic
* fix watch paths logic
* import REPO_ROOT from correct package
* create the @kbn/crypto package
* update core's `dev` config
* only export bootstrap function
* extract sslConfig to http-tools package
* fix core types
* fix optimizer tests
* fix cli_dev_mode tests
* fix basePath proxy tests
* update generated doc
* fix unit tests
* create @kbn/dev-cli-mode package
* remove useless comment
* self-review NITS
* update CODEOWNERS file
* add devOnly flag
* use variable for DEV_MODE_PATH
* review comments
* fix logger/log adapter
* fix log calls in base path proxy server
* address some review comments
* rename @kbn/http-tools to @kbn/server-http-tools
* more review comments
* move test to correct file
* add comment on getBootstrapScript
* fix lint
* lint
* add cli-dev-mode to eslint dev packages
* review comments
* update yarn.lock
* Revert "[ci] skip building ts refs when not necessary (#95739)"
This reverts commit e46a74f7
# Conflicts:
# .github/CODEOWNERS
This commit is contained in:
parent
16b8c0213a
commit
41eb6e2b12
|
@ -93,6 +93,7 @@ const SAFER_LODASH_SET_DEFINITELYTYPED_HEADER = `
|
|||
const DEV_PACKAGES = [
|
||||
'kbn-babel-code-parser',
|
||||
'kbn-dev-utils',
|
||||
'kbn-cli-dev-mode',
|
||||
'kbn-docs-utils',
|
||||
'kbn-es*',
|
||||
'kbn-eslint*',
|
||||
|
|
|
@ -10,10 +10,10 @@ Set of helpers used to create `KibanaResponse` to form HTTP response on an incom
|
|||
|
||||
```typescript
|
||||
kibanaResponseFactory: {
|
||||
custom: <T extends string | Record<string, any> | Buffer | Error | Stream | {
|
||||
custom: <T extends string | Record<string, any> | Error | Buffer | {
|
||||
message: string | Error;
|
||||
attributes?: Record<string, any> | undefined;
|
||||
} | undefined>(options: CustomHttpResponseOptions<T>) => KibanaResponse<T>;
|
||||
} | Stream | undefined>(options: CustomHttpResponseOptions<T>) => KibanaResponse<T>;
|
||||
badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
|
|
|
@ -124,11 +124,13 @@
|
|||
"@kbn/apm-utils": "link:packages/kbn-apm-utils",
|
||||
"@kbn/config": "link:packages/kbn-config",
|
||||
"@kbn/config-schema": "link:packages/kbn-config-schema",
|
||||
"@kbn/crypto": "link:packages/kbn-crypto",
|
||||
"@kbn/i18n": "link:packages/kbn-i18n",
|
||||
"@kbn/interpreter": "link:packages/kbn-interpreter",
|
||||
"@kbn/legacy-logging": "link:packages/kbn-legacy-logging",
|
||||
"@kbn/logging": "link:packages/kbn-logging",
|
||||
"@kbn/monaco": "link:packages/kbn-monaco",
|
||||
"@kbn/server-http-tools": "link:packages/kbn-server-http-tools",
|
||||
"@kbn/std": "link:packages/kbn-std",
|
||||
"@kbn/tinymath": "link:packages/kbn-tinymath",
|
||||
"@kbn/ui-framework": "link:packages/kbn-ui-framework",
|
||||
|
@ -448,6 +450,7 @@
|
|||
"@jest/reporters": "^26.5.2",
|
||||
"@kbn/babel-code-parser": "link:packages/kbn-babel-code-parser",
|
||||
"@kbn/babel-preset": "link:packages/kbn-babel-preset",
|
||||
"@kbn/cli-dev-mode": "link:packages/kbn-cli-dev-mode",
|
||||
"@kbn/dev-utils": "link:packages/kbn-dev-utils",
|
||||
"@kbn/docs-utils": "link:packages/kbn-docs-utils",
|
||||
"@kbn/es": "link:packages/kbn-es",
|
||||
|
|
|
@ -26,8 +26,12 @@ The `DevServer` object is responsible for everything related to running and rest
|
|||
|
||||
The `Optimizer` object manages a `@kbn/optimizer` instance, adapting its configuration and logging to the data available to the CLI.
|
||||
|
||||
## `BasePathProxyServer` (currently passed from core)
|
||||
## `BasePathProxyServer`
|
||||
|
||||
The `BasePathProxyServer` is passed to the `CliDevMode` from core when the dev mode is trigged by the `--dev` flag. This proxy injects a random three character base path in the URL that Kibana is served from to help ensure that Kibana features are written to adapt to custom base path configurations from users.
|
||||
This proxy injects a random three character base path in the URL that Kibana is served from to help ensure that Kibana features
|
||||
are written to adapt to custom base path configurations from users.
|
||||
|
||||
The basePathProxy also has another important job, ensuring that requests don't fail because the server is restarting and that the browser receives front-end assets containing all saved changes. We accomplish this by observing the ready state of the `Optimizer` and `DevServer` objects and pausing all requests through the proxy until both objects report that they aren't building/restarting based on recently saved changes.
|
||||
The basePathProxy also has another important job, ensuring that requests don't fail because the server is restarting and
|
||||
that the browser receives front-end assets containing all saved changes. We accomplish this by observing the ready state of
|
||||
the `Optimizer` and `DevServer` objects and pausing all requests through the proxy until both objects report that
|
||||
they aren't building/restarting based on recently saved changes.
|
13
packages/kbn-cli-dev-mode/jest.config.js
Normal file
13
packages/kbn-cli-dev-mode/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-cli-dev-mode'],
|
||||
};
|
26
packages/kbn-cli-dev-mode/package.json
Normal file
26
packages/kbn-cli-dev-mode/package.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "@kbn/cli-dev-mode",
|
||||
"main": "./target/index.js",
|
||||
"types": "./target/index.d.ts",
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc",
|
||||
"kbn:bootstrap": "yarn build",
|
||||
"kbn:watch": "yarn build --watch"
|
||||
},
|
||||
"kibana": {
|
||||
"devOnly": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@kbn/config": "link:../kbn-config",
|
||||
"@kbn/config-schema": "link:../kbn-config-schema",
|
||||
"@kbn/logging": "link:../kbn-logging",
|
||||
"@kbn/server-http-tools": "link:../kbn-server-http-tools",
|
||||
"@kbn/optimizer": "link:../kbn-optimizer",
|
||||
"@kbn/std": "link:../kbn-std",
|
||||
"@kbn/dev-utils": "link:../kbn-dev-utils",
|
||||
"@kbn/utils": "link:../kbn-utils"
|
||||
}
|
||||
}
|
358
packages/kbn-cli-dev-mode/src/base_path_proxy_server.test.ts
Normal file
358
packages/kbn-cli-dev-mode/src/base_path_proxy_server.test.ts
Normal file
|
@ -0,0 +1,358 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Server } from '@hapi/hapi';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import supertest from 'supertest';
|
||||
import {
|
||||
getServerOptions,
|
||||
getListenerOptions,
|
||||
createServer,
|
||||
IHttpConfig,
|
||||
} from '@kbn/server-http-tools';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
|
||||
import { BasePathProxyServer, BasePathProxyServerOptions } from './base_path_proxy_server';
|
||||
import { DevConfig } from './config/dev_config';
|
||||
import { TestLog } from './log';
|
||||
|
||||
describe('BasePathProxyServer', () => {
|
||||
let server: Server;
|
||||
let proxyServer: BasePathProxyServer;
|
||||
let logger: TestLog;
|
||||
let config: IHttpConfig;
|
||||
let basePath: string;
|
||||
let proxySupertest: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
beforeEach(async () => {
|
||||
logger = new TestLog();
|
||||
|
||||
config = {
|
||||
host: '127.0.0.1',
|
||||
port: 10012,
|
||||
keepaliveTimeout: 1000,
|
||||
socketTimeout: 1000,
|
||||
cors: {
|
||||
enabled: false,
|
||||
allowCredentials: false,
|
||||
allowOrigin: [],
|
||||
},
|
||||
ssl: { enabled: false },
|
||||
maxPayload: new ByteSizeValue(1024),
|
||||
};
|
||||
|
||||
const serverOptions = getServerOptions(config);
|
||||
const listenerOptions = getListenerOptions(config);
|
||||
server = createServer(serverOptions, listenerOptions);
|
||||
|
||||
// setup and start the proxy server
|
||||
const proxyConfig: IHttpConfig = { ...config, port: 10013 };
|
||||
const devConfig = new DevConfig({ basePathProxyTarget: config.port });
|
||||
proxyServer = new BasePathProxyServer(logger, proxyConfig, devConfig);
|
||||
const options: BasePathProxyServerOptions = {
|
||||
shouldRedirectFromOldBasePath: () => true,
|
||||
delayUntil: () => EMPTY,
|
||||
};
|
||||
await proxyServer.start(options);
|
||||
|
||||
// set the base path or throw if for some unknown reason it is not setup
|
||||
if (proxyServer.basePath == null) {
|
||||
throw new Error('Invalid null base path, all tests will fail');
|
||||
} else {
|
||||
basePath = proxyServer.basePath;
|
||||
}
|
||||
proxySupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
await proxyServer.stop();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('root URL will return a 302 redirect', async () => {
|
||||
await proxySupertest.get('/').expect(302);
|
||||
});
|
||||
|
||||
test('root URL will return a redirect location with exactly 3 characters that are a-z', async () => {
|
||||
const res = await proxySupertest.get('/');
|
||||
const location = res.header.location;
|
||||
expect(location).toMatch(/[a-z]{3}/);
|
||||
});
|
||||
|
||||
test('forwards request with the correct path', async () => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${basePath}/foo/{test}`,
|
||||
handler: (request, h) => {
|
||||
return h.response(request.params.test);
|
||||
},
|
||||
});
|
||||
await server.start();
|
||||
|
||||
await proxySupertest
|
||||
.get(`${basePath}/foo/some-string`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.text).toBe('some-string');
|
||||
});
|
||||
});
|
||||
|
||||
test('forwards request with the correct query params', async () => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${basePath}/foo/`,
|
||||
handler: (request, h) => {
|
||||
return h.response(request.query);
|
||||
},
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
await proxySupertest
|
||||
.get(`${basePath}/foo/?bar=test&quux=123`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toEqual({ bar: 'test', quux: '123' });
|
||||
});
|
||||
});
|
||||
|
||||
test('forwards the request body', async () => {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${basePath}/foo/`,
|
||||
handler: (request, h) => {
|
||||
return h.response(request.payload);
|
||||
},
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
await proxySupertest
|
||||
.post(`${basePath}/foo/`)
|
||||
.send({
|
||||
bar: 'test',
|
||||
baz: 123,
|
||||
})
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toEqual({ bar: 'test', baz: 123 });
|
||||
});
|
||||
});
|
||||
|
||||
test('returns the correct status code', async () => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${basePath}/foo/`,
|
||||
handler: (request, h) => {
|
||||
return h.response({ foo: 'bar' }).code(417);
|
||||
},
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
await proxySupertest
|
||||
.get(`${basePath}/foo/`)
|
||||
.expect(417)
|
||||
.then((res) => {
|
||||
expect(res.body).toEqual({ foo: 'bar' });
|
||||
});
|
||||
});
|
||||
|
||||
test('returns the response headers', async () => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${basePath}/foo/`,
|
||||
handler: (request, h) => {
|
||||
return h.response({ foo: 'bar' }).header('foo', 'bar');
|
||||
},
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
await proxySupertest
|
||||
.get(`${basePath}/foo/`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.get('foo')).toEqual('bar');
|
||||
});
|
||||
});
|
||||
|
||||
test('handles putting', async () => {
|
||||
server.route({
|
||||
method: 'PUT',
|
||||
path: `${basePath}/foo/`,
|
||||
handler: (request, h) => {
|
||||
return h.response(request.payload);
|
||||
},
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
await proxySupertest
|
||||
.put(`${basePath}/foo/`)
|
||||
.send({
|
||||
bar: 'test',
|
||||
baz: 123,
|
||||
})
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toEqual({ bar: 'test', baz: 123 });
|
||||
});
|
||||
});
|
||||
|
||||
test('handles deleting', async () => {
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${basePath}/foo/{test}`,
|
||||
handler: (request, h) => {
|
||||
return h.response(request.params.test);
|
||||
},
|
||||
});
|
||||
await server.start();
|
||||
|
||||
await proxySupertest
|
||||
.delete(`${basePath}/foo/some-string`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.text).toBe('some-string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with `basepath: /bar` and `rewriteBasePath: false`', () => {
|
||||
beforeEach(async () => {
|
||||
const configWithBasePath: IHttpConfig = {
|
||||
...config,
|
||||
basePath: '/bar',
|
||||
rewriteBasePath: false,
|
||||
} as IHttpConfig;
|
||||
|
||||
const serverOptions = getServerOptions(configWithBasePath);
|
||||
const listenerOptions = getListenerOptions(configWithBasePath);
|
||||
server = createServer(serverOptions, listenerOptions);
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${basePath}/`,
|
||||
handler: (request, h) => {
|
||||
return h.response('value:/');
|
||||
},
|
||||
});
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${basePath}/foo`,
|
||||
handler: (request, h) => {
|
||||
return h.response('value:/foo');
|
||||
},
|
||||
});
|
||||
|
||||
await server.start();
|
||||
});
|
||||
|
||||
test('/bar => 404', async () => {
|
||||
await proxySupertest.get(`${basePath}/bar`).expect(404);
|
||||
});
|
||||
|
||||
test('/bar/ => 404', async () => {
|
||||
await proxySupertest.get(`${basePath}/bar/`).expect(404);
|
||||
});
|
||||
|
||||
test('/bar/foo => 404', async () => {
|
||||
await proxySupertest.get(`${basePath}/bar/foo`).expect(404);
|
||||
});
|
||||
|
||||
test('/ => /', async () => {
|
||||
await proxySupertest
|
||||
.get(`${basePath}/`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.text).toBe('value:/');
|
||||
});
|
||||
});
|
||||
|
||||
test('/foo => /foo', async () => {
|
||||
await proxySupertest
|
||||
.get(`${basePath}/foo`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.text).toBe('value:/foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRedirect', () => {
|
||||
let proxyServerWithoutShouldRedirect: BasePathProxyServer;
|
||||
let proxyWithoutShouldRedirectSupertest: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
beforeEach(async () => {
|
||||
// setup and start a proxy server which does not use "shouldRedirectFromOldBasePath"
|
||||
const proxyConfig: IHttpConfig = { ...config, port: 10004 };
|
||||
const devConfig = new DevConfig({ basePathProxyTarget: config.port });
|
||||
proxyServerWithoutShouldRedirect = new BasePathProxyServer(logger, proxyConfig, devConfig);
|
||||
const options: Readonly<BasePathProxyServerOptions> = {
|
||||
shouldRedirectFromOldBasePath: () => false, // Return false to not redirect
|
||||
delayUntil: () => EMPTY,
|
||||
};
|
||||
await proxyServerWithoutShouldRedirect.start(options);
|
||||
proxyWithoutShouldRedirectSupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await proxyServerWithoutShouldRedirect.stop();
|
||||
});
|
||||
|
||||
test('it will do a redirect if it detects what looks like a stale or previously used base path', async () => {
|
||||
const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg';
|
||||
const res = await proxySupertest.get(`/${fakeBasePath}`).expect(302);
|
||||
const location = res.header.location;
|
||||
expect(location).toEqual(`${basePath}/`);
|
||||
});
|
||||
|
||||
test('it will NOT do a redirect if it detects what looks like a stale or previously used base path if we intentionally turn it off', async () => {
|
||||
const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg';
|
||||
await proxyWithoutShouldRedirectSupertest.get(`/${fakeBasePath}`).expect(404);
|
||||
});
|
||||
|
||||
test('it will NOT redirect if it detects a larger path than 3 characters', async () => {
|
||||
await proxySupertest.get('/abcde').expect(404);
|
||||
});
|
||||
|
||||
test('it will NOT redirect if it is not a GET verb', async () => {
|
||||
const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg';
|
||||
await proxySupertest.put(`/${fakeBasePath}`).expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor option for sending in a custom basePath', () => {
|
||||
let proxyServerWithFooBasePath: BasePathProxyServer;
|
||||
let proxyWithFooBasePath: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
beforeEach(async () => {
|
||||
// setup and start a proxy server which uses a basePath of "foo"
|
||||
const proxyConfig = { ...config, port: 10004, basePath: '/foo' }; // <-- "foo" here in basePath
|
||||
const devConfig = new DevConfig({ basePathProxyTarget: config.port });
|
||||
proxyServerWithFooBasePath = new BasePathProxyServer(logger, proxyConfig, devConfig);
|
||||
const options: Readonly<BasePathProxyServerOptions> = {
|
||||
shouldRedirectFromOldBasePath: () => true,
|
||||
delayUntil: () => EMPTY,
|
||||
};
|
||||
await proxyServerWithFooBasePath.start(options);
|
||||
proxyWithFooBasePath = supertest(`http://127.0.0.1:${proxyConfig.port}`);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await proxyServerWithFooBasePath.stop();
|
||||
});
|
||||
|
||||
test('it will do a redirect to foo which is our passed in value for the configuration', async () => {
|
||||
const res = await proxyWithFooBasePath.get('/bar').expect(302);
|
||||
const location = res.header.location;
|
||||
expect(location).toEqual('/foo/');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,21 +8,21 @@
|
|||
|
||||
import Url from 'url';
|
||||
import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https';
|
||||
|
||||
import apm from 'elastic-apm-node';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import { Server, Request } from '@hapi/hapi';
|
||||
import HapiProxy from '@hapi/h2o2';
|
||||
import { sampleSize } from 'lodash';
|
||||
import * as Rx from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import { createServer, getListenerOptions, getServerOptions } from '@kbn/server-http-tools';
|
||||
|
||||
import { DevConfig } from '../dev';
|
||||
import { Logger } from '../logging';
|
||||
import { HttpConfig } from './http_config';
|
||||
import { createServer, getListenerOptions, getServerOptions } from './http_tools';
|
||||
import { DevConfig, HttpConfig } from './config';
|
||||
import { Log } from './log';
|
||||
|
||||
const ONE_GIGABYTE = 1024 * 1024 * 1024;
|
||||
const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split('');
|
||||
const getRandomBasePath = () => sampleSize(alphabet, 3).join('');
|
||||
|
||||
export interface BasePathProxyServerOptions {
|
||||
shouldRedirectFromOldBasePath: (path: string) => boolean;
|
||||
|
@ -30,9 +30,22 @@ export interface BasePathProxyServerOptions {
|
|||
}
|
||||
|
||||
export class BasePathProxyServer {
|
||||
private readonly httpConfig: HttpConfig;
|
||||
private server?: Server;
|
||||
private httpsAgent?: HttpsAgent;
|
||||
|
||||
constructor(
|
||||
private readonly log: Log,
|
||||
httpConfig: HttpConfig,
|
||||
private readonly devConfig: DevConfig
|
||||
) {
|
||||
this.httpConfig = {
|
||||
...httpConfig,
|
||||
maxPayload: new ByteSizeValue(ONE_GIGABYTE),
|
||||
basePath: httpConfig.basePath ?? `/${getRandomBasePath()}`,
|
||||
};
|
||||
}
|
||||
|
||||
public get basePath() {
|
||||
return this.httpConfig.basePath;
|
||||
}
|
||||
|
@ -49,21 +62,8 @@ export class BasePathProxyServer {
|
|||
return this.httpConfig.port;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly log: Logger,
|
||||
private readonly httpConfig: HttpConfig,
|
||||
private readonly devConfig: DevConfig
|
||||
) {
|
||||
const ONE_GIGABYTE = 1024 * 1024 * 1024;
|
||||
httpConfig.maxPayload = new ByteSizeValue(ONE_GIGABYTE);
|
||||
|
||||
if (!httpConfig.basePath) {
|
||||
httpConfig.basePath = `/${sampleSize(alphabet, 3).join('')}`;
|
||||
}
|
||||
}
|
||||
|
||||
public async start(options: Readonly<BasePathProxyServerOptions>) {
|
||||
this.log.debug('starting basepath proxy server');
|
||||
public async start(options: BasePathProxyServerOptions) {
|
||||
this.log.write('starting basepath proxy server');
|
||||
|
||||
const serverOptions = getServerOptions(this.httpConfig);
|
||||
const listenerOptions = getListenerOptions(this.httpConfig);
|
||||
|
@ -88,7 +88,7 @@ export class BasePathProxyServer {
|
|||
|
||||
await this.server.start();
|
||||
|
||||
this.log.info(
|
||||
this.log.write(
|
||||
`basepath proxy server running at ${Url.format({
|
||||
host: this.server.info.uri,
|
||||
pathname: this.httpConfig.basePath,
|
||||
|
@ -101,7 +101,7 @@ export class BasePathProxyServer {
|
|||
return;
|
||||
}
|
||||
|
||||
this.log.debug('stopping basepath proxy server');
|
||||
this.log.write('stopping basepath proxy server');
|
||||
await this.server.stop();
|
||||
this.server = undefined;
|
||||
|
43
packages/kbn-cli-dev-mode/src/bootstrap.ts
Normal file
43
packages/kbn-cli-dev-mode/src/bootstrap.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { REPO_ROOT } from '@kbn/utils';
|
||||
import { CliArgs, Env, RawConfigAdapter } from '@kbn/config';
|
||||
import { CliDevMode } from './cli_dev_mode';
|
||||
import { CliLog } from './log';
|
||||
import { convertToLogger } from './log_adapter';
|
||||
import { loadConfig } from './config';
|
||||
|
||||
interface BootstrapArgs {
|
||||
configs: string[];
|
||||
cliArgs: CliArgs;
|
||||
applyConfigOverrides: RawConfigAdapter;
|
||||
}
|
||||
|
||||
export async function bootstrapDevMode({ configs, cliArgs, applyConfigOverrides }: BootstrapArgs) {
|
||||
const log = new CliLog(!!cliArgs.quiet, !!cliArgs.silent);
|
||||
|
||||
const env = Env.createDefault(REPO_ROOT, {
|
||||
configs,
|
||||
cliArgs,
|
||||
});
|
||||
|
||||
const config = await loadConfig({
|
||||
env,
|
||||
logger: convertToLogger(log),
|
||||
rawConfigAdapter: applyConfigOverrides,
|
||||
});
|
||||
|
||||
const cliDevMode = new CliDevMode({
|
||||
cliArgs,
|
||||
config,
|
||||
log,
|
||||
});
|
||||
|
||||
await cliDevMode.start();
|
||||
}
|
|
@ -7,16 +7,16 @@
|
|||
*/
|
||||
|
||||
import Path from 'path';
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import {
|
||||
REPO_ROOT,
|
||||
createAbsolutePathSerializer,
|
||||
createAnyInstanceSerializer,
|
||||
} from '@kbn/dev-utils';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import { TestLog } from './log';
|
||||
import { CliDevMode } from './cli_dev_mode';
|
||||
import { CliDevMode, SomeCliArgs } from './cli_dev_mode';
|
||||
import type { CliDevConfig } from './config';
|
||||
|
||||
expect.addSnapshotSerializer(createAbsolutePathSerializer());
|
||||
expect.addSnapshotSerializer(createAnyInstanceSerializer(Rx.Observable, 'Rx.Observable'));
|
||||
|
@ -31,6 +31,9 @@ const { Optimizer } = jest.requireMock('./optimizer');
|
|||
jest.mock('./dev_server');
|
||||
const { DevServer } = jest.requireMock('./dev_server');
|
||||
|
||||
jest.mock('./base_path_proxy_server');
|
||||
const { BasePathProxyServer } = jest.requireMock('./base_path_proxy_server');
|
||||
|
||||
jest.mock('@kbn/dev-utils/target/ci_stats_reporter');
|
||||
const { CiStatsReporter } = jest.requireMock('@kbn/dev-utils/target/ci_stats_reporter');
|
||||
|
||||
|
@ -41,13 +44,6 @@ jest.mock('./get_server_watch_paths', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
process.argv = ['node', './script', 'foo', 'bar', 'baz'];
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const log = new TestLog();
|
||||
|
||||
const mockBasePathProxy = {
|
||||
targetPort: 9999,
|
||||
basePath: '/foo/bar',
|
||||
|
@ -55,26 +51,53 @@ const mockBasePathProxy = {
|
|||
stop: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
let log: TestLog;
|
||||
|
||||
beforeEach(() => {
|
||||
process.argv = ['node', './script', 'foo', 'bar', 'baz'];
|
||||
log = new TestLog();
|
||||
BasePathProxyServer.mockImplementation(() => mockBasePathProxy);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockBasePathProxy.start.mockReset();
|
||||
mockBasePathProxy.stop.mockReset();
|
||||
});
|
||||
|
||||
const createCliArgs = (parts: Partial<SomeCliArgs> = {}): SomeCliArgs => ({
|
||||
basePath: false,
|
||||
cache: true,
|
||||
disableOptimizer: false,
|
||||
dist: true,
|
||||
oss: true,
|
||||
pluginPaths: [],
|
||||
pluginScanDirs: [Path.resolve(REPO_ROOT, 'src/plugins')],
|
||||
quiet: false,
|
||||
silent: false,
|
||||
runExamples: false,
|
||||
watch: true,
|
||||
log,
|
||||
};
|
||||
silent: false,
|
||||
quiet: false,
|
||||
...parts,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
log.messages.length = 0;
|
||||
const createDevConfig = (parts: Partial<CliDevConfig> = {}): CliDevConfig => ({
|
||||
plugins: {
|
||||
pluginSearchPaths: [Path.resolve(REPO_ROOT, 'src/plugins')],
|
||||
additionalPluginPaths: [],
|
||||
},
|
||||
dev: {
|
||||
basePathProxyTargetPort: 9000,
|
||||
},
|
||||
http: {} as any,
|
||||
...parts,
|
||||
});
|
||||
|
||||
const createOptions = ({ cliArgs = {} }: { cliArgs?: Partial<SomeCliArgs> } = {}) => ({
|
||||
cliArgs: createCliArgs(cliArgs),
|
||||
config: createDevConfig(),
|
||||
log,
|
||||
});
|
||||
|
||||
it('passes correct args to sub-classes', () => {
|
||||
new CliDevMode(defaultOptions);
|
||||
new CliDevMode(createOptions());
|
||||
|
||||
expect(DevServer.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -105,6 +128,9 @@ it('passes correct args to sub-classes', () => {
|
|||
"enabled": true,
|
||||
"oss": true,
|
||||
"pluginPaths": Array [],
|
||||
"pluginScanDirs": Array [
|
||||
<absolute path>/src/plugins,
|
||||
],
|
||||
"quiet": false,
|
||||
"repoRoot": <absolute path>,
|
||||
"runExamples": false,
|
||||
|
@ -131,33 +157,38 @@ it('passes correct args to sub-classes', () => {
|
|||
],
|
||||
]
|
||||
`);
|
||||
|
||||
expect(BasePathProxyServer).not.toHaveBeenCalled();
|
||||
|
||||
expect(log.messages).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
|
||||
it('disables the optimizer', () => {
|
||||
new CliDevMode({
|
||||
...defaultOptions,
|
||||
disableOptimizer: true,
|
||||
});
|
||||
new CliDevMode(createOptions({ cliArgs: { disableOptimizer: true } }));
|
||||
|
||||
expect(Optimizer.mock.calls[0][0]).toHaveProperty('enabled', false);
|
||||
});
|
||||
|
||||
it('disables the watcher', () => {
|
||||
new CliDevMode({
|
||||
...defaultOptions,
|
||||
watch: false,
|
||||
});
|
||||
new CliDevMode(createOptions({ cliArgs: { watch: false } }));
|
||||
|
||||
expect(Optimizer.mock.calls[0][0]).toHaveProperty('watch', false);
|
||||
expect(Watcher.mock.calls[0][0]).toHaveProperty('enabled', false);
|
||||
});
|
||||
|
||||
it('overrides the basePath of the server when basePathProxy is defined', () => {
|
||||
new CliDevMode({
|
||||
...defaultOptions,
|
||||
basePathProxy: mockBasePathProxy as any,
|
||||
});
|
||||
it('enables the basePath proxy', () => {
|
||||
new CliDevMode(createOptions({ cliArgs: { basePath: true } }));
|
||||
|
||||
expect(BasePathProxyServer).toHaveBeenCalledTimes(1);
|
||||
expect(BasePathProxyServer.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
<TestLog>,
|
||||
Object {},
|
||||
Object {
|
||||
"basePathProxyTargetPort": 9000,
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(DevServer.mock.calls[0][0].argv).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -229,9 +260,7 @@ describe('#start()/#stop()', () => {
|
|||
});
|
||||
|
||||
it('logs a warning if basePathProxy is not passed', () => {
|
||||
new CliDevMode({
|
||||
...defaultOptions,
|
||||
}).start();
|
||||
new CliDevMode(createOptions()).start();
|
||||
|
||||
expect(log.messages).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -261,16 +290,9 @@ describe('#start()/#stop()', () => {
|
|||
});
|
||||
|
||||
it('calls start on BasePathProxy if enabled', () => {
|
||||
const basePathProxy: any = {
|
||||
start: jest.fn(),
|
||||
};
|
||||
new CliDevMode(createOptions({ cliArgs: { basePath: true } })).start();
|
||||
|
||||
new CliDevMode({
|
||||
...defaultOptions,
|
||||
basePathProxy,
|
||||
}).start();
|
||||
|
||||
expect(basePathProxy.start.mock.calls).toMatchInlineSnapshot(`
|
||||
expect(mockBasePathProxy.start.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
|
@ -283,7 +305,7 @@ describe('#start()/#stop()', () => {
|
|||
});
|
||||
|
||||
it('subscribes to Optimizer#run$, Watcher#run$, and DevServer#run$', () => {
|
||||
new CliDevMode(defaultOptions).start();
|
||||
new CliDevMode(createOptions()).start();
|
||||
|
||||
expect(optimizerRun$.observers).toHaveLength(1);
|
||||
expect(watcherRun$.observers).toHaveLength(1);
|
||||
|
@ -291,10 +313,7 @@ describe('#start()/#stop()', () => {
|
|||
});
|
||||
|
||||
it('logs an error and exits the process if Optimizer#run$ errors', () => {
|
||||
new CliDevMode({
|
||||
...defaultOptions,
|
||||
basePathProxy: mockBasePathProxy as any,
|
||||
}).start();
|
||||
new CliDevMode(createOptions({ cliArgs: { basePath: true } })).start();
|
||||
|
||||
expect(processExitMock).not.toHaveBeenCalled();
|
||||
optimizerRun$.error({ stack: 'Error: foo bar' });
|
||||
|
@ -319,10 +338,7 @@ describe('#start()/#stop()', () => {
|
|||
});
|
||||
|
||||
it('logs an error and exits the process if Watcher#run$ errors', () => {
|
||||
new CliDevMode({
|
||||
...defaultOptions,
|
||||
basePathProxy: mockBasePathProxy as any,
|
||||
}).start();
|
||||
new CliDevMode(createOptions({ cliArgs: { basePath: true } })).start();
|
||||
|
||||
expect(processExitMock).not.toHaveBeenCalled();
|
||||
watcherRun$.error({ stack: 'Error: foo bar' });
|
||||
|
@ -347,10 +363,7 @@ describe('#start()/#stop()', () => {
|
|||
});
|
||||
|
||||
it('logs an error and exits the process if DevServer#run$ errors', () => {
|
||||
new CliDevMode({
|
||||
...defaultOptions,
|
||||
basePathProxy: mockBasePathProxy as any,
|
||||
}).start();
|
||||
new CliDevMode(createOptions({ cliArgs: { basePath: true } })).start();
|
||||
|
||||
expect(processExitMock).not.toHaveBeenCalled();
|
||||
devServerRun$.error({ stack: 'Error: foo bar' });
|
||||
|
@ -376,10 +389,7 @@ describe('#start()/#stop()', () => {
|
|||
|
||||
it('throws if start() has already been called', () => {
|
||||
expect(() => {
|
||||
const devMode = new CliDevMode({
|
||||
...defaultOptions,
|
||||
basePathProxy: mockBasePathProxy as any,
|
||||
});
|
||||
const devMode = new CliDevMode(createOptions({ cliArgs: { basePath: true } }));
|
||||
|
||||
devMode.start();
|
||||
devMode.start();
|
||||
|
@ -387,10 +397,7 @@ describe('#start()/#stop()', () => {
|
|||
});
|
||||
|
||||
it('unsubscribes from all observables and stops basePathProxy when stopped', () => {
|
||||
const devMode = new CliDevMode({
|
||||
...defaultOptions,
|
||||
basePathProxy: mockBasePathProxy as any,
|
||||
});
|
||||
const devMode = new CliDevMode(createOptions({ cliArgs: { basePath: true } }));
|
||||
|
||||
devMode.start();
|
||||
devMode.stop();
|
|
@ -7,8 +7,6 @@
|
|||
*/
|
||||
|
||||
import Path from 'path';
|
||||
|
||||
import { REPO_ROOT, CiStatsReporter } from '@kbn/dev-utils';
|
||||
import * as Rx from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
|
@ -20,24 +18,32 @@ import {
|
|||
switchMap,
|
||||
concatMap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { CliArgs } from '../../core/server/config';
|
||||
import { LegacyConfig } from '../../core/server/legacy';
|
||||
import { BasePathProxyServer } from '../../core/server/http';
|
||||
import { CliArgs } from '@kbn/config';
|
||||
import { REPO_ROOT, CiStatsReporter } from '@kbn/dev-utils';
|
||||
|
||||
import { Log, CliLog } from './log';
|
||||
import { Optimizer } from './optimizer';
|
||||
import { DevServer } from './dev_server';
|
||||
import { Watcher } from './watcher';
|
||||
import { BasePathProxyServer } from './base_path_proxy_server';
|
||||
import { shouldRedirectFromOldBasePath } from './should_redirect_from_old_base_path';
|
||||
import { getServerWatchPaths } from './get_server_watch_paths';
|
||||
import { CliDevConfig } from './config';
|
||||
|
||||
// timeout where the server is allowed to exit gracefully
|
||||
const GRACEFUL_TIMEOUT = 5000;
|
||||
|
||||
export type SomeCliArgs = Pick<
|
||||
CliArgs,
|
||||
'quiet' | 'silent' | 'disableOptimizer' | 'watch' | 'oss' | 'runExamples' | 'cache' | 'dist'
|
||||
| 'quiet'
|
||||
| 'silent'
|
||||
| 'disableOptimizer'
|
||||
| 'watch'
|
||||
| 'oss'
|
||||
| 'runExamples'
|
||||
| 'cache'
|
||||
| 'dist'
|
||||
| 'basePath'
|
||||
>;
|
||||
|
||||
export interface CliDevModeOptions {
|
||||
|
@ -76,49 +82,28 @@ const firstAllTrue = (...sources: Array<Rx.Observable<boolean>>) =>
|
|||
*
|
||||
*/
|
||||
export class CliDevMode {
|
||||
static fromCoreServices(
|
||||
cliArgs: SomeCliArgs,
|
||||
config: LegacyConfig,
|
||||
basePathProxy?: BasePathProxyServer
|
||||
) {
|
||||
new CliDevMode({
|
||||
quiet: !!cliArgs.quiet,
|
||||
silent: !!cliArgs.silent,
|
||||
cache: !!cliArgs.cache,
|
||||
disableOptimizer: !!cliArgs.disableOptimizer,
|
||||
dist: !!cliArgs.dist,
|
||||
oss: !!cliArgs.oss,
|
||||
runExamples: !!cliArgs.runExamples,
|
||||
pluginPaths: config.get<string[]>('plugins.paths'),
|
||||
pluginScanDirs: config.get<string[]>('plugins.scanDirs'),
|
||||
watch: !!cliArgs.watch,
|
||||
basePathProxy,
|
||||
}).start();
|
||||
}
|
||||
private readonly log: Log;
|
||||
private readonly basePathProxy?: BasePathProxyServer;
|
||||
private readonly watcher: Watcher;
|
||||
private readonly devServer: DevServer;
|
||||
private readonly optimizer: Optimizer;
|
||||
private startTime?: number;
|
||||
|
||||
private subscription?: Rx.Subscription;
|
||||
|
||||
constructor(options: CliDevModeOptions) {
|
||||
this.basePathProxy = options.basePathProxy;
|
||||
this.log = options.log || new CliLog(!!options.quiet, !!options.silent);
|
||||
constructor({ cliArgs, config, log }: { cliArgs: SomeCliArgs; config: CliDevConfig; log?: Log }) {
|
||||
this.log = log || new CliLog(!!cliArgs.quiet, !!cliArgs.silent);
|
||||
|
||||
if (cliArgs.basePath) {
|
||||
this.basePathProxy = new BasePathProxyServer(this.log, config.http, config.dev);
|
||||
}
|
||||
|
||||
const { watchPaths, ignorePaths } = getServerWatchPaths({
|
||||
pluginPaths: options.pluginPaths ?? [],
|
||||
pluginScanDirs: [
|
||||
...(options.pluginScanDirs ?? []),
|
||||
Path.resolve(REPO_ROOT, 'src/plugins'),
|
||||
Path.resolve(REPO_ROOT, 'x-pack/plugins'),
|
||||
],
|
||||
pluginPaths: config.plugins.additionalPluginPaths,
|
||||
pluginScanDirs: config.plugins.pluginSearchPaths,
|
||||
});
|
||||
|
||||
this.watcher = new Watcher({
|
||||
enabled: !!options.watch,
|
||||
enabled: !!cliArgs.watch,
|
||||
log: this.log,
|
||||
cwd: REPO_ROOT,
|
||||
paths: watchPaths,
|
||||
|
@ -133,10 +118,10 @@ export class CliDevMode {
|
|||
script: Path.resolve(REPO_ROOT, 'scripts/kibana'),
|
||||
argv: [
|
||||
...process.argv.slice(2).filter((v) => v !== '--no-watch'),
|
||||
...(options.basePathProxy
|
||||
...(this.basePathProxy
|
||||
? [
|
||||
`--server.port=${options.basePathProxy.targetPort}`,
|
||||
`--server.basePath=${options.basePathProxy.basePath}`,
|
||||
`--server.port=${this.basePathProxy.targetPort}`,
|
||||
`--server.basePath=${this.basePathProxy.basePath}`,
|
||||
'--server.rewriteBasePath=true',
|
||||
]
|
||||
: []),
|
||||
|
@ -153,16 +138,17 @@ export class CliDevMode {
|
|||
});
|
||||
|
||||
this.optimizer = new Optimizer({
|
||||
enabled: !options.disableOptimizer,
|
||||
enabled: !cliArgs.disableOptimizer,
|
||||
repoRoot: REPO_ROOT,
|
||||
oss: options.oss,
|
||||
pluginPaths: options.pluginPaths,
|
||||
runExamples: options.runExamples,
|
||||
cache: options.cache,
|
||||
dist: options.dist,
|
||||
quiet: options.quiet,
|
||||
silent: options.silent,
|
||||
watch: options.watch,
|
||||
oss: cliArgs.oss,
|
||||
pluginPaths: config.plugins.additionalPluginPaths,
|
||||
pluginScanDirs: config.plugins.pluginSearchPaths,
|
||||
runExamples: cliArgs.runExamples,
|
||||
cache: cliArgs.cache,
|
||||
dist: cliArgs.dist,
|
||||
quiet: !!cliArgs.quiet,
|
||||
silent: !!cliArgs.silent,
|
||||
watch: cliArgs.watch,
|
||||
});
|
||||
}
|
||||
|
28
packages/kbn-cli-dev-mode/src/config/dev_config.ts
Normal file
28
packages/kbn-cli-dev-mode/src/config/dev_config.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const devConfigSchema = schema.object(
|
||||
{
|
||||
basePathProxyTarget: schema.number({
|
||||
defaultValue: 5603,
|
||||
}),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
export type DevConfigType = TypeOf<typeof devConfigSchema>;
|
||||
|
||||
export class DevConfig {
|
||||
public basePathProxyTargetPort: number;
|
||||
|
||||
constructor(rawConfig: DevConfigType) {
|
||||
this.basePathProxyTargetPort = rawConfig.basePathProxyTarget;
|
||||
}
|
||||
}
|
65
packages/kbn-cli-dev-mode/src/config/http_config.ts
Normal file
65
packages/kbn-cli-dev-mode/src/config/http_config.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema';
|
||||
import { ICorsConfig, IHttpConfig, ISslConfig, SslConfig, sslSchema } from '@kbn/server-http-tools';
|
||||
|
||||
export const httpConfigSchema = schema.object(
|
||||
{
|
||||
host: schema.string({
|
||||
defaultValue: 'localhost',
|
||||
hostname: true,
|
||||
}),
|
||||
basePath: schema.maybe(schema.string()),
|
||||
port: schema.number({
|
||||
defaultValue: 5601,
|
||||
}),
|
||||
maxPayload: schema.byteSize({
|
||||
defaultValue: '1048576b',
|
||||
}),
|
||||
keepaliveTimeout: schema.number({
|
||||
defaultValue: 120000,
|
||||
}),
|
||||
socketTimeout: schema.number({
|
||||
defaultValue: 120000,
|
||||
}),
|
||||
cors: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
allowCredentials: schema.boolean({ defaultValue: false }),
|
||||
allowOrigin: schema.arrayOf(schema.string(), {
|
||||
defaultValue: ['*'],
|
||||
}),
|
||||
}),
|
||||
ssl: sslSchema,
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
export type HttpConfigType = TypeOf<typeof httpConfigSchema>;
|
||||
|
||||
export class HttpConfig implements IHttpConfig {
|
||||
basePath?: string;
|
||||
host: string;
|
||||
port: number;
|
||||
maxPayload: ByteSizeValue;
|
||||
keepaliveTimeout: number;
|
||||
socketTimeout: number;
|
||||
cors: ICorsConfig;
|
||||
ssl: ISslConfig;
|
||||
|
||||
constructor(rawConfig: HttpConfigType) {
|
||||
this.basePath = rawConfig.basePath;
|
||||
this.host = rawConfig.host;
|
||||
this.port = rawConfig.port;
|
||||
this.maxPayload = rawConfig.maxPayload;
|
||||
this.keepaliveTimeout = rawConfig.keepaliveTimeout;
|
||||
this.socketTimeout = rawConfig.socketTimeout;
|
||||
this.cors = rawConfig.cors;
|
||||
this.ssl = new SslConfig(rawConfig.ssl);
|
||||
}
|
||||
}
|
13
packages/kbn-cli-dev-mode/src/config/index.ts
Normal file
13
packages/kbn-cli-dev-mode/src/config/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type { DevConfig } from './dev_config';
|
||||
export type { PluginsConfig } from './plugins_config';
|
||||
export type { HttpConfig } from './http_config';
|
||||
export type { CliDevConfig } from './types';
|
||||
export { loadConfig } from './load_config';
|
44
packages/kbn-cli-dev-mode/src/config/load_config.ts
Normal file
44
packages/kbn-cli-dev-mode/src/config/load_config.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Env, RawConfigService, ConfigService, RawConfigAdapter } from '@kbn/config';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { devConfigSchema, DevConfig, DevConfigType } from './dev_config';
|
||||
import { httpConfigSchema, HttpConfig, HttpConfigType } from './http_config';
|
||||
import { pluginsConfigSchema, PluginsConfig, PluginsConfigType } from './plugins_config';
|
||||
import { CliDevConfig } from './types';
|
||||
|
||||
export const loadConfig = async ({
|
||||
env,
|
||||
logger,
|
||||
rawConfigAdapter,
|
||||
}: {
|
||||
env: Env;
|
||||
logger: Logger;
|
||||
rawConfigAdapter: RawConfigAdapter;
|
||||
}): Promise<CliDevConfig> => {
|
||||
const rawConfigService = new RawConfigService(env.configs, rawConfigAdapter);
|
||||
rawConfigService.loadConfig();
|
||||
|
||||
const configService = new ConfigService(rawConfigService, env, logger);
|
||||
configService.setSchema('dev', devConfigSchema);
|
||||
configService.setSchema('plugins', pluginsConfigSchema);
|
||||
configService.setSchema('http', httpConfigSchema);
|
||||
|
||||
await configService.validate();
|
||||
|
||||
const devConfig = configService.atPathSync<DevConfigType>('dev');
|
||||
const pluginsConfig = configService.atPathSync<PluginsConfigType>('plugins');
|
||||
const httpConfig = configService.atPathSync<HttpConfigType>('http');
|
||||
|
||||
return {
|
||||
dev: new DevConfig(devConfig),
|
||||
plugins: new PluginsConfig(pluginsConfig, env),
|
||||
http: new HttpConfig(httpConfig),
|
||||
};
|
||||
};
|
37
packages/kbn-cli-dev-mode/src/config/plugins_config.ts
Normal file
37
packages/kbn-cli-dev-mode/src/config/plugins_config.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { Env } from '@kbn/config';
|
||||
|
||||
export const pluginsConfigSchema = schema.object(
|
||||
{
|
||||
paths: schema.arrayOf(schema.string(), { defaultValue: [] }),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
export type PluginsConfigType = TypeOf<typeof pluginsConfigSchema>;
|
||||
|
||||
/** @internal */
|
||||
export class PluginsConfig {
|
||||
/**
|
||||
* Defines directories that we should scan for the plugin subdirectories.
|
||||
*/
|
||||
public readonly pluginSearchPaths: string[];
|
||||
|
||||
/**
|
||||
* Defines directories where an additional plugin exists.
|
||||
*/
|
||||
public readonly additionalPluginPaths: string[];
|
||||
|
||||
constructor(rawConfig: PluginsConfigType, env: Env) {
|
||||
this.pluginSearchPaths = [...env.pluginSearchPaths];
|
||||
this.additionalPluginPaths = rawConfig.paths;
|
||||
}
|
||||
}
|
17
packages/kbn-cli-dev-mode/src/config/types.ts
Normal file
17
packages/kbn-cli-dev-mode/src/config/types.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { DevConfig } from './dev_config';
|
||||
import type { HttpConfig } from './http_config';
|
||||
import type { PluginsConfig } from './plugins_config';
|
||||
|
||||
export interface CliDevConfig {
|
||||
dev: DevConfig;
|
||||
http: HttpConfig;
|
||||
plugins: PluginsConfig;
|
||||
}
|
|
@ -47,15 +47,7 @@ export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) {
|
|||
...pluginScanDirs,
|
||||
].map((path) => Path.resolve(path))
|
||||
)
|
||||
);
|
||||
|
||||
for (const watchPath of watchPaths) {
|
||||
if (!Fs.existsSync(fromRoot(watchPath))) {
|
||||
throw new Error(
|
||||
`A watch directory [${watchPath}] does not exist, which will cause chokidar to fail. Either make sure the directory exists or remove it as a watch source in the ClusterManger`
|
||||
);
|
||||
}
|
||||
}
|
||||
).filter((path) => Fs.existsSync(fromRoot(path)));
|
||||
|
||||
const ignorePaths = [
|
||||
/[\\\/](\..*|node_modules|bower_components|target|public|__[a-z0-9_]+__|coverage)([\\\/]|$)/,
|
|
@ -6,5 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './cli_dev_mode';
|
||||
export * from './log';
|
||||
export { bootstrapDevMode } from './bootstrap';
|
28
packages/kbn-cli-dev-mode/src/log_adapter.ts
Normal file
28
packages/kbn-cli-dev-mode/src/log_adapter.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { Log } from './log';
|
||||
|
||||
export const convertToLogger = (cliLog: Log): Logger => {
|
||||
const getErrorMessage = (msgOrError: string | Error): string => {
|
||||
return typeof msgOrError === 'string' ? msgOrError : msgOrError.message;
|
||||
};
|
||||
|
||||
const adapter: Logger = {
|
||||
trace: (message) => cliLog.write(message),
|
||||
debug: (message) => cliLog.write(message),
|
||||
info: (message) => cliLog.write(message),
|
||||
warn: (msgOrError) => cliLog.warn('warning', getErrorMessage(msgOrError)),
|
||||
error: (msgOrError) => cliLog.bad('error', getErrorMessage(msgOrError)),
|
||||
fatal: (msgOrError) => cliLog.bad('fatal', getErrorMessage(msgOrError)),
|
||||
log: (record) => cliLog.write(record.message),
|
||||
get: () => adapter,
|
||||
};
|
||||
return adapter;
|
||||
};
|
|
@ -43,6 +43,7 @@ const defaultOptions: Options = {
|
|||
dist: true,
|
||||
oss: true,
|
||||
pluginPaths: ['/some/dir'],
|
||||
pluginScanDirs: ['/some-scan-path'],
|
||||
quiet: true,
|
||||
silent: true,
|
||||
repoRoot: '/app',
|
||||
|
@ -83,6 +84,7 @@ it('uses options to create valid OptimizerConfig', () => {
|
|||
runExamples: false,
|
||||
oss: false,
|
||||
pluginPaths: [],
|
||||
pluginScanDirs: [],
|
||||
repoRoot: '/foo/bar',
|
||||
watch: false,
|
||||
});
|
||||
|
@ -99,6 +101,9 @@ it('uses options to create valid OptimizerConfig', () => {
|
|||
"pluginPaths": Array [
|
||||
"/some/dir",
|
||||
],
|
||||
"pluginScanDirs": Array [
|
||||
"/some-scan-path",
|
||||
],
|
||||
"repoRoot": "/app",
|
||||
"watch": true,
|
||||
},
|
||||
|
@ -111,6 +116,7 @@ it('uses options to create valid OptimizerConfig', () => {
|
|||
"includeCoreBundle": true,
|
||||
"oss": false,
|
||||
"pluginPaths": Array [],
|
||||
"pluginScanDirs": Array [],
|
||||
"repoRoot": "/foo/bar",
|
||||
"watch": false,
|
||||
},
|
|
@ -31,6 +31,7 @@ export interface Options {
|
|||
oss: boolean;
|
||||
runExamples: boolean;
|
||||
pluginPaths: string[];
|
||||
pluginScanDirs: string[];
|
||||
writeLogTo?: Writable;
|
||||
}
|
||||
|
||||
|
@ -56,6 +57,7 @@ export class Optimizer {
|
|||
oss: options.oss,
|
||||
examples: options.runExamples,
|
||||
pluginPaths: options.pluginPaths,
|
||||
pluginScanDirs: options.pluginScanDirs,
|
||||
});
|
||||
|
||||
const dim = Chalk.dim('np bld');
|
11
packages/kbn-cli-dev-mode/tsconfig.json
Normal file
11
packages/kbn-cli-dev-mode/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "./target",
|
||||
"declarationMap": true,
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"exclude": ["target"]
|
||||
}
|
|
@ -30,6 +30,5 @@ export function getEnvOptions(options: DeepPartial<EnvOptions> = {}): EnvOptions
|
|||
runExamples: false,
|
||||
...(options.cliArgs || {}),
|
||||
},
|
||||
isDevCliParent: options.isDevCliParent !== undefined ? options.isDevCliParent : false,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ Env {
|
|||
"/some/other/path/some-kibana.yml",
|
||||
],
|
||||
"homeDir": "/test/kibanaRoot",
|
||||
"isDevCliParent": false,
|
||||
"logDir": "/test/kibanaRoot/log",
|
||||
"mode": Object {
|
||||
"dev": true,
|
||||
|
@ -65,7 +64,6 @@ Env {
|
|||
"/some/other/path/some-kibana.yml",
|
||||
],
|
||||
"homeDir": "/test/kibanaRoot",
|
||||
"isDevCliParent": false,
|
||||
"logDir": "/test/kibanaRoot/log",
|
||||
"mode": Object {
|
||||
"dev": false,
|
||||
|
@ -108,7 +106,6 @@ Env {
|
|||
"/test/cwd/config/kibana.yml",
|
||||
],
|
||||
"homeDir": "/test/kibanaRoot",
|
||||
"isDevCliParent": true,
|
||||
"logDir": "/test/kibanaRoot/log",
|
||||
"mode": Object {
|
||||
"dev": true,
|
||||
|
@ -151,7 +148,6 @@ Env {
|
|||
"/some/other/path/some-kibana.yml",
|
||||
],
|
||||
"homeDir": "/test/kibanaRoot",
|
||||
"isDevCliParent": false,
|
||||
"logDir": "/test/kibanaRoot/log",
|
||||
"mode": Object {
|
||||
"dev": false,
|
||||
|
@ -194,7 +190,6 @@ Env {
|
|||
"/some/other/path/some-kibana.yml",
|
||||
],
|
||||
"homeDir": "/test/kibanaRoot",
|
||||
"isDevCliParent": false,
|
||||
"logDir": "/test/kibanaRoot/log",
|
||||
"mode": Object {
|
||||
"dev": false,
|
||||
|
@ -237,7 +232,6 @@ Env {
|
|||
"/some/other/path/some-kibana.yml",
|
||||
],
|
||||
"homeDir": "/some/home/dir",
|
||||
"isDevCliParent": false,
|
||||
"logDir": "/some/home/dir/log",
|
||||
"mode": Object {
|
||||
"dev": false,
|
||||
|
|
|
@ -36,7 +36,6 @@ test('correctly creates default environment in dev mode.', () => {
|
|||
REPO_ROOT,
|
||||
getEnvOptions({
|
||||
configs: ['/test/cwd/config/kibana.yml'],
|
||||
isDevCliParent: true,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import { PackageInfo, EnvironmentMode } from './types';
|
|||
export interface EnvOptions {
|
||||
configs: string[];
|
||||
cliArgs: CliArgs;
|
||||
isDevCliParent: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -89,12 +88,6 @@ export class Env {
|
|||
*/
|
||||
public readonly configs: readonly string[];
|
||||
|
||||
/**
|
||||
* Indicates that this Kibana instance is running in the parent process of the dev cli.
|
||||
* @internal
|
||||
*/
|
||||
public readonly isDevCliParent: boolean;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -111,7 +104,6 @@ export class Env {
|
|||
|
||||
this.cliArgs = Object.freeze(options.cliArgs);
|
||||
this.configs = Object.freeze(options.configs);
|
||||
this.isDevCliParent = options.isDevCliParent;
|
||||
|
||||
const isDevMode = this.cliArgs.dev || this.cliArgs.envName === 'development';
|
||||
this.mode = Object.freeze<EnvironmentMode>({
|
||||
|
|
|
@ -16,7 +16,12 @@ export {
|
|||
ConfigDeprecationWithContext,
|
||||
} from './deprecation';
|
||||
|
||||
export { RawConfigurationProvider, RawConfigService, getConfigFromFiles } from './raw';
|
||||
export {
|
||||
RawConfigurationProvider,
|
||||
RawConfigService,
|
||||
RawConfigAdapter,
|
||||
getConfigFromFiles,
|
||||
} from './raw';
|
||||
|
||||
export { ConfigService, IConfigService } from './config_service';
|
||||
export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config';
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { RawConfigService, RawConfigurationProvider } from './raw_config_service';
|
||||
export { RawConfigService, RawConfigurationProvider, RawConfigAdapter } from './raw_config_service';
|
||||
export { getConfigFromFiles } from './read_config';
|
||||
|
|
|
@ -13,7 +13,7 @@ import typeDetect from 'type-detect';
|
|||
|
||||
import { getConfigFromFiles } from './read_config';
|
||||
|
||||
type RawConfigAdapter = (rawConfig: Record<string, any>) => Record<string, any>;
|
||||
export type RawConfigAdapter = (rawConfig: Record<string, any>) => Record<string, any>;
|
||||
|
||||
export type RawConfigurationProvider = Pick<RawConfigService, 'getConfig$'>;
|
||||
|
||||
|
|
3
packages/kbn-crypto/README.md
Normal file
3
packages/kbn-crypto/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/crypto
|
||||
|
||||
Crypto tools and utilities for Kibana
|
13
packages/kbn-crypto/jest.config.js
Normal file
13
packages/kbn-crypto/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-crypto'],
|
||||
};
|
16
packages/kbn-crypto/package.json
Normal file
16
packages/kbn-crypto/package.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@kbn/crypto",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"main": "./target/index.js",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc",
|
||||
"kbn:bootstrap": "yarn build",
|
||||
"kbn:watch": "yarn build --watch"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@kbn/dev-utils": "link:../kbn-dev-utils"
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ import {
|
|||
import { NO_CA_PATH, NO_CERT_PATH, NO_KEY_PATH, TWO_CAS_PATH, TWO_KEYS_PATH } from './__fixtures__';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from './index';
|
||||
import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from './pkcs12';
|
||||
|
||||
const reformatPem = (pem: string) => {
|
||||
// ensure consistency in line endings when comparing two PEM files
|
11
packages/kbn-crypto/tsconfig.json
Normal file
11
packages/kbn-crypto/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
3
packages/kbn-server-http-tools/README.md
Normal file
3
packages/kbn-server-http-tools/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/http-tools
|
||||
|
||||
Http utilities for core and the basepath server
|
13
packages/kbn-server-http-tools/jest.config.js
Normal file
13
packages/kbn-server-http-tools/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-server-http-tools'],
|
||||
};
|
20
packages/kbn-server-http-tools/package.json
Normal file
20
packages/kbn-server-http-tools/package.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@kbn/server-http-tools",
|
||||
"main": "./target/index.js",
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "rm -rf target && ../../node_modules/.bin/tsc",
|
||||
"kbn:bootstrap": "yarn build",
|
||||
"kbn:watch": "yarn build --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kbn/config-schema": "link:../kbn-config-schema",
|
||||
"@kbn/crypto": "link:../kbn-crypto",
|
||||
"@kbn/std": "link:../kbn-std"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kbn/utility-types": "link:../kbn-utility-types"
|
||||
}
|
||||
}
|
29
packages/kbn-server-http-tools/src/create_server.ts
Normal file
29
packages/kbn-server-http-tools/src/create_server.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Server, ServerOptions } from '@hapi/hapi';
|
||||
import { ListenerOptions } from './get_listener_options';
|
||||
|
||||
export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) {
|
||||
const server = new Server(serverOptions);
|
||||
|
||||
server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout;
|
||||
server.listener.setTimeout(listenerOptions.socketTimeout);
|
||||
server.listener.on('timeout', (socket) => {
|
||||
socket.destroy();
|
||||
});
|
||||
server.listener.on('clientError', (err, socket) => {
|
||||
if (socket.writable) {
|
||||
socket.end(Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii'));
|
||||
} else {
|
||||
socket.destroy(err);
|
||||
}
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { Request, ResponseToolkit } from '@hapi/hapi';
|
||||
import {
|
||||
defaultValidationErrorHandler,
|
||||
HapiValidationError,
|
||||
} from './default_validation_error_handler';
|
||||
|
||||
const emptyOutput = {
|
||||
statusCode: 400,
|
||||
headers: {},
|
||||
payload: {
|
||||
statusCode: 400,
|
||||
error: '',
|
||||
validation: {
|
||||
source: '',
|
||||
keys: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('defaultValidationErrorHandler', () => {
|
||||
it('formats value validation errors correctly', () => {
|
||||
expect.assertions(1);
|
||||
const schema = Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
}).required()
|
||||
);
|
||||
|
||||
const error = schema.validate([{}], { abortEarly: false }).error as HapiValidationError;
|
||||
|
||||
// Emulate what Hapi v17 does by default
|
||||
error.output = { ...emptyOutput };
|
||||
error.output.payload.validation.keys = ['0.type', ''];
|
||||
|
||||
try {
|
||||
defaultValidationErrorHandler({} as Request, {} as ResponseToolkit, error);
|
||||
} catch (err) {
|
||||
// Verify the empty string gets corrected to 'value'
|
||||
expect(err.output.payload.validation.keys).toEqual(['0.type', 'value']);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Lifecycle, Request, ResponseToolkit, Util } from '@hapi/hapi';
|
||||
import { ValidationError } from 'joi';
|
||||
import Hoek from '@hapi/hoek';
|
||||
|
||||
/**
|
||||
* Hapi extends the ValidationError interface to add this output key with more data.
|
||||
*/
|
||||
export interface HapiValidationError extends ValidationError {
|
||||
output: {
|
||||
statusCode: number;
|
||||
headers: Util.Dictionary<string | string[]>;
|
||||
payload: {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message?: string;
|
||||
validation: {
|
||||
source: string;
|
||||
keys: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to replicate Hapi v16 and below's validation responses. Should be used in the routes.validate.failAction key.
|
||||
*/
|
||||
export function defaultValidationErrorHandler(
|
||||
request: Request,
|
||||
h: ResponseToolkit,
|
||||
err?: Error
|
||||
): Lifecycle.ReturnValue {
|
||||
// Newer versions of Joi don't format the key for missing params the same way. This shim
|
||||
// provides backwards compatibility. Unfortunately, Joi doesn't export it's own Error class
|
||||
// in JS so we have to rely on the `name` key before we can cast it.
|
||||
//
|
||||
// The Hapi code we're 'overwriting' can be found here:
|
||||
// https://github.com/hapijs/hapi/blob/master/lib/validation.js#L102
|
||||
if (err && err.name === 'ValidationError' && err.hasOwnProperty('output')) {
|
||||
const validationError: HapiValidationError = err as HapiValidationError;
|
||||
const validationKeys: string[] = [];
|
||||
|
||||
validationError.details.forEach((detail) => {
|
||||
if (detail.path.length > 0) {
|
||||
validationKeys.push(Hoek.escapeHtml(detail.path.join('.')));
|
||||
} else {
|
||||
// If no path, use the value sigil to signal the entire value had an issue.
|
||||
validationKeys.push('value');
|
||||
}
|
||||
});
|
||||
|
||||
validationError.output.payload.validation.keys = validationKeys;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
21
packages/kbn-server-http-tools/src/get_listener_options.ts
Normal file
21
packages/kbn-server-http-tools/src/get_listener_options.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { IHttpConfig } from './types';
|
||||
|
||||
export interface ListenerOptions {
|
||||
keepaliveTimeout: number;
|
||||
socketTimeout: number;
|
||||
}
|
||||
|
||||
export function getListenerOptions(config: IHttpConfig): ListenerOptions {
|
||||
return {
|
||||
keepaliveTimeout: config.keepaliveTimeout,
|
||||
socketTimeout: config.socketTimeout,
|
||||
};
|
||||
}
|
85
packages/kbn-server-http-tools/src/get_request_id.test.ts
Normal file
85
packages/kbn-server-http-tools/src/get_request_id.test.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getRequestId } from './get_request_id';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'),
|
||||
}));
|
||||
|
||||
describe('getRequestId', () => {
|
||||
describe('when allowFromAnyIp is true', () => {
|
||||
it('generates a UUID if no x-opaque-id header is present', () => {
|
||||
const request = {
|
||||
headers: {},
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual(
|
||||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses x-opaque-id header value if present', () => {
|
||||
const request = {
|
||||
headers: {
|
||||
'x-opaque-id': 'id from header',
|
||||
},
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual(
|
||||
'id from header'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when allowFromAnyIp is false', () => {
|
||||
describe('and ipAllowlist is empty', () => {
|
||||
it('generates a UUID even if x-opaque-id header is present', () => {
|
||||
const request = {
|
||||
headers: { 'x-opaque-id': 'id from header' },
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: [] })).toEqual(
|
||||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and ipAllowlist is not empty', () => {
|
||||
it('uses x-opaque-id header if request comes from trusted IP address', () => {
|
||||
const request = {
|
||||
headers: { 'x-opaque-id': 'id from header' },
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual(
|
||||
'id from header'
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a UUID if request comes from untrusted IP address', () => {
|
||||
const request = {
|
||||
headers: { 'x-opaque-id': 'id from header' },
|
||||
raw: { req: { socket: { remoteAddress: '5.5.5.5' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual(
|
||||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
);
|
||||
});
|
||||
|
||||
it('generates UUID if request comes from trusted IP address but no x-opaque-id header is present', () => {
|
||||
const request = {
|
||||
headers: {},
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual(
|
||||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
22
packages/kbn-server-http-tools/src/get_request_id.ts
Normal file
22
packages/kbn-server-http-tools/src/get_request_id.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Request } from '@hapi/hapi';
|
||||
import uuid from 'uuid';
|
||||
|
||||
export function getRequestId(
|
||||
request: Request,
|
||||
{ allowFromAnyIp, ipAllowlist }: { allowFromAnyIp: boolean; ipAllowlist: string[] }
|
||||
): string {
|
||||
const remoteAddress = request.raw.req.socket?.remoteAddress;
|
||||
return allowFromAnyIp ||
|
||||
// socket may be undefined in integration tests that connect via the http listener directly
|
||||
(remoteAddress && ipAllowlist.includes(remoteAddress))
|
||||
? request.headers['x-opaque-id'] ?? uuid.v4()
|
||||
: uuid.v4();
|
||||
}
|
122
packages/kbn-server-http-tools/src/get_server_options.test.ts
Normal file
122
packages/kbn-server-http-tools/src/get_server_options.test.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import { getServerOptions } from './get_server_options';
|
||||
import { IHttpConfig } from './types';
|
||||
|
||||
jest.mock('fs', () => {
|
||||
const original = jest.requireActual('fs');
|
||||
return {
|
||||
// Hapi Inert patches native methods
|
||||
...original,
|
||||
readFileSync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const createConfig = (parts: Partial<IHttpConfig>): IHttpConfig => ({
|
||||
host: 'localhost',
|
||||
port: 5601,
|
||||
socketTimeout: 120000,
|
||||
keepaliveTimeout: 120000,
|
||||
maxPayload: ByteSizeValue.parse('1048576b'),
|
||||
...parts,
|
||||
cors: {
|
||||
enabled: false,
|
||||
allowCredentials: false,
|
||||
allowOrigin: ['*'],
|
||||
...parts.cors,
|
||||
},
|
||||
ssl: {
|
||||
enabled: false,
|
||||
...parts.ssl,
|
||||
},
|
||||
});
|
||||
|
||||
describe('getServerOptions', () => {
|
||||
beforeEach(() =>
|
||||
jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`)
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('properly configures TLS with default options', () => {
|
||||
const httpConfig = createConfig({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": undefined,
|
||||
"cert": "some-certificate-path",
|
||||
"ciphers": undefined,
|
||||
"honorCipherOrder": true,
|
||||
"key": "some-key-path",
|
||||
"passphrase": undefined,
|
||||
"rejectUnauthorized": undefined,
|
||||
"requestCert": undefined,
|
||||
"secureOptions": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('properly configures TLS with client authentication', () => {
|
||||
const httpConfig = createConfig({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
certificateAuthorities: ['ca-1', 'ca-2'],
|
||||
cipherSuites: ['suite-a', 'suite-b'],
|
||||
keyPassphrase: 'passPhrase',
|
||||
rejectUnauthorized: true,
|
||||
requestCert: true,
|
||||
getSecureOptions: () => 42,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": Array [
|
||||
"ca-1",
|
||||
"ca-2",
|
||||
],
|
||||
"cert": "some-certificate-path",
|
||||
"ciphers": "suite-a:suite-b",
|
||||
"honorCipherOrder": true,
|
||||
"key": "some-key-path",
|
||||
"passphrase": "passPhrase",
|
||||
"rejectUnauthorized": true,
|
||||
"requestCert": true,
|
||||
"secureOptions": 42,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('properly configures CORS when cors enabled', () => {
|
||||
const httpConfig = createConfig({
|
||||
cors: {
|
||||
enabled: true,
|
||||
allowCredentials: false,
|
||||
allowOrigin: ['*'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getServerOptions(httpConfig).routes?.cors).toEqual({
|
||||
credentials: false,
|
||||
origin: ['*'],
|
||||
headers: ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'],
|
||||
});
|
||||
});
|
||||
});
|
75
packages/kbn-server-http-tools/src/get_server_options.ts
Normal file
75
packages/kbn-server-http-tools/src/get_server_options.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { RouteOptionsCors, ServerOptions } from '@hapi/hapi';
|
||||
import { ServerOptions as TLSOptions } from 'https';
|
||||
import { defaultValidationErrorHandler } from './default_validation_error_handler';
|
||||
import { IHttpConfig } from './types';
|
||||
|
||||
const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'];
|
||||
|
||||
/**
|
||||
* Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server.
|
||||
*/
|
||||
export function getServerOptions(config: IHttpConfig, { configureTLS = true } = {}) {
|
||||
const cors: RouteOptionsCors | false = config.cors.enabled
|
||||
? {
|
||||
credentials: config.cors.allowCredentials,
|
||||
origin: config.cors.allowOrigin,
|
||||
headers: corsAllowedHeaders,
|
||||
}
|
||||
: false;
|
||||
const options: ServerOptions = {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
routes: {
|
||||
cache: {
|
||||
privacy: 'private',
|
||||
otherwise: 'private, no-cache, no-store, must-revalidate',
|
||||
},
|
||||
cors,
|
||||
payload: {
|
||||
maxBytes: config.maxPayload.getValueInBytes(),
|
||||
},
|
||||
validate: {
|
||||
failAction: defaultValidationErrorHandler,
|
||||
options: {
|
||||
abortEarly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
state: {
|
||||
strictHeader: false,
|
||||
isHttpOnly: true,
|
||||
isSameSite: false, // necessary to allow using Kibana inside an iframe
|
||||
},
|
||||
};
|
||||
|
||||
if (configureTLS && config.ssl.enabled) {
|
||||
const ssl = config.ssl;
|
||||
|
||||
// TODO: Hapi types have a typo in `tls` property type definition: `https.RequestOptions` is used instead of
|
||||
// `https.ServerOptions`, and `honorCipherOrder` isn't presented in `https.RequestOptions`.
|
||||
const tlsOptions: TLSOptions = {
|
||||
ca: ssl.certificateAuthorities,
|
||||
cert: ssl.certificate,
|
||||
ciphers: config.ssl.cipherSuites?.join(':'),
|
||||
// We use the server's cipher order rather than the client's to prevent the BEAST attack.
|
||||
honorCipherOrder: true,
|
||||
key: ssl.key,
|
||||
passphrase: ssl.keyPassphrase,
|
||||
secureOptions: ssl.getSecureOptions ? ssl.getSecureOptions() : undefined,
|
||||
requestCert: ssl.requestCert,
|
||||
rejectUnauthorized: ssl.rejectUnauthorized,
|
||||
};
|
||||
|
||||
options.tls = tlsOptions;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
15
packages/kbn-server-http-tools/src/index.ts
Normal file
15
packages/kbn-server-http-tools/src/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type { IHttpConfig, ISslConfig, ICorsConfig } from './types';
|
||||
export { createServer } from './create_server';
|
||||
export { defaultValidationErrorHandler } from './default_validation_error_handler';
|
||||
export { getListenerOptions } from './get_listener_options';
|
||||
export { getServerOptions } from './get_server_options';
|
||||
export { getRequestId } from './get_request_id';
|
||||
export { sslSchema, SslConfig } from './ssl';
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { CliDevMode } from '../../../dev/cli_dev_mode';
|
||||
export { SslConfig, sslSchema } from './ssl_config';
|
|
@ -13,7 +13,7 @@ jest.mock('fs', () => {
|
|||
|
||||
export const mockReadPkcs12Keystore = jest.fn();
|
||||
export const mockReadPkcs12Truststore = jest.fn();
|
||||
jest.mock('../utils', () => ({
|
||||
jest.mock('@kbn/crypto', () => ({
|
||||
readPkcs12Keystore: mockReadPkcs12Keystore,
|
||||
readPkcs12Truststore: mockReadPkcs12Truststore,
|
||||
}));
|
|
@ -34,7 +34,7 @@ describe('#SslConfig', () => {
|
|||
beforeEach(() => {
|
||||
const realFs = jest.requireActual('fs');
|
||||
mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path));
|
||||
const utils = jest.requireActual('../utils');
|
||||
const utils = jest.requireActual('@kbn/crypto');
|
||||
mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) =>
|
||||
utils.readPkcs12Keystore(path, password)
|
||||
);
|
|
@ -7,9 +7,9 @@
|
|||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { readPkcs12Keystore, readPkcs12Truststore } from '@kbn/crypto';
|
||||
import { constants as cryptoConstants } from 'crypto';
|
||||
import { readFileSync } from 'fs';
|
||||
import { readPkcs12Keystore, readPkcs12Truststore } from '../utils';
|
||||
|
||||
const protocolMap = new Map<string, number>([
|
||||
['TLSv1', cryptoConstants.SSL_OP_NO_TLSv1],
|
||||
|
@ -81,14 +81,13 @@ type SslConfigType = TypeOf<typeof sslSchema>;
|
|||
|
||||
export class SslConfig {
|
||||
public enabled: boolean;
|
||||
public redirectHttpFromPort: number | undefined;
|
||||
public key: string | undefined;
|
||||
public certificate: string | undefined;
|
||||
public certificateAuthorities: string[] | undefined;
|
||||
public keyPassphrase: string | undefined;
|
||||
public redirectHttpFromPort?: number;
|
||||
public key?: string;
|
||||
public certificate?: string;
|
||||
public certificateAuthorities?: string[];
|
||||
public keyPassphrase?: string;
|
||||
public requestCert: boolean;
|
||||
public rejectUnauthorized: boolean;
|
||||
|
||||
public cipherSuites: string[];
|
||||
public supportedProtocols: string[];
|
||||
|
||||
|
@ -164,6 +163,4 @@ export class SslConfig {
|
|||
}
|
||||
}
|
||||
|
||||
const readFile = (file: string) => {
|
||||
return readFileSync(file, 'utf8');
|
||||
};
|
||||
const readFile = (file: string) => readFileSync(file, 'utf8');
|
37
packages/kbn-server-http-tools/src/types.ts
Normal file
37
packages/kbn-server-http-tools/src/types.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
|
||||
export interface IHttpConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
maxPayload: ByteSizeValue;
|
||||
keepaliveTimeout: number;
|
||||
socketTimeout: number;
|
||||
cors: ICorsConfig;
|
||||
ssl: ISslConfig;
|
||||
}
|
||||
|
||||
export interface ICorsConfig {
|
||||
enabled: boolean;
|
||||
allowCredentials: boolean;
|
||||
allowOrigin: string[];
|
||||
}
|
||||
|
||||
export interface ISslConfig {
|
||||
enabled: boolean;
|
||||
key?: string;
|
||||
certificate?: string;
|
||||
certificateAuthorities?: string[];
|
||||
cipherSuites?: string[];
|
||||
keyPassphrase?: string;
|
||||
requestCert?: boolean;
|
||||
rejectUnauthorized?: boolean;
|
||||
getSecureOptions?: () => number;
|
||||
}
|
14
packages/kbn-server-http-tools/tsconfig.json
Normal file
14
packages/kbn-server-http-tools/tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@kbn/std": "link:../kbn-std"
|
||||
}
|
||||
}
|
|
@ -57,3 +57,5 @@ const { kibanaDir, kibanaPkgJson } = findKibanaPackageJson();
|
|||
|
||||
export const REPO_ROOT = kibanaDir;
|
||||
export const UPSTREAM_BRANCH = kibanaPkgJson.branch;
|
||||
|
||||
export const fromRoot = (...paths: string[]) => Path.resolve(REPO_ROOT, ...paths);
|
||||
|
|
|
@ -12,10 +12,8 @@ import { statSync } from 'fs';
|
|||
import { resolve } from 'path';
|
||||
import url from 'url';
|
||||
|
||||
import { getConfigPath } from '@kbn/utils';
|
||||
import { getConfigPath, fromRoot } from '@kbn/utils';
|
||||
import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils';
|
||||
import { fromRoot } from '../../core/server/utils';
|
||||
import { bootstrap } from '../../core/server';
|
||||
import { readKeystore } from '../keystore/read_keystore';
|
||||
|
||||
function canRequire(path) {
|
||||
|
@ -31,9 +29,21 @@ function canRequire(path) {
|
|||
}
|
||||
}
|
||||
|
||||
const DEV_MODE_PATH = resolve(__dirname, '../../dev/cli_dev_mode');
|
||||
const DEV_MODE_PATH = '@kbn/cli-dev-mode';
|
||||
const DEV_MODE_SUPPORTED = canRequire(DEV_MODE_PATH);
|
||||
|
||||
const getBootstrapScript = (isDev) => {
|
||||
if (DEV_MODE_SUPPORTED && isDev && process.env.isDevCliChild !== 'true') {
|
||||
// need dynamic require to exclude it from production build
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
const { bootstrapDevMode } = require(DEV_MODE_PATH);
|
||||
return bootstrapDevMode;
|
||||
} else {
|
||||
const { bootstrap } = require('../../core/server');
|
||||
return bootstrap;
|
||||
}
|
||||
};
|
||||
|
||||
const pathCollector = function () {
|
||||
const paths = [];
|
||||
return function (path) {
|
||||
|
@ -79,6 +89,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
|
|||
throw new Error(`Can't use --ssl when "${path}" configuration is already defined.`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureNotDefined('server.ssl.certificate');
|
||||
ensureNotDefined('server.ssl.key');
|
||||
ensureNotDefined('server.ssl.keystore.path');
|
||||
|
@ -210,31 +221,40 @@ export default function (program) {
|
|||
}
|
||||
|
||||
const unknownOptions = this.getUnknownOptions();
|
||||
await bootstrap({
|
||||
configs: [].concat(opts.config || []),
|
||||
cliArgs: {
|
||||
dev: !!opts.dev,
|
||||
envName: unknownOptions.env ? unknownOptions.env.name : undefined,
|
||||
// no longer supported
|
||||
quiet: !!opts.quiet,
|
||||
silent: !!opts.silent,
|
||||
watch: !!opts.watch,
|
||||
runExamples: !!opts.runExamples,
|
||||
// We want to run without base path when the `--run-examples` flag is given so that we can use local
|
||||
// links in other documentation sources, like "View this tutorial [here](http://localhost:5601/app/tutorial/xyz)".
|
||||
// We can tell users they only have to run with `yarn start --run-examples` to get those
|
||||
// local links to work. Similar to what we do for "View in Console" links in our
|
||||
// elastic.co links.
|
||||
basePath: opts.runExamples ? false : !!opts.basePath,
|
||||
optimize: !!opts.optimize,
|
||||
disableOptimizer: !opts.optimizer,
|
||||
oss: !!opts.oss,
|
||||
cache: !!opts.cache,
|
||||
dist: !!opts.dist,
|
||||
},
|
||||
features: {
|
||||
isCliDevModeSupported: DEV_MODE_SUPPORTED,
|
||||
},
|
||||
const configs = [].concat(opts.config || []);
|
||||
const cliArgs = {
|
||||
dev: !!opts.dev,
|
||||
envName: unknownOptions.env ? unknownOptions.env.name : undefined,
|
||||
// no longer supported
|
||||
quiet: !!opts.quiet,
|
||||
silent: !!opts.silent,
|
||||
watch: !!opts.watch,
|
||||
runExamples: !!opts.runExamples,
|
||||
// We want to run without base path when the `--run-examples` flag is given so that we can use local
|
||||
// links in other documentation sources, like "View this tutorial [here](http://localhost:5601/app/tutorial/xyz)".
|
||||
// We can tell users they only have to run with `yarn start --run-examples` to get those
|
||||
// local links to work. Similar to what we do for "View in Console" links in our
|
||||
// elastic.co links.
|
||||
basePath: opts.runExamples ? false : !!opts.basePath,
|
||||
optimize: !!opts.optimize,
|
||||
disableOptimizer: !opts.optimizer,
|
||||
oss: !!opts.oss,
|
||||
cache: !!opts.cache,
|
||||
dist: !!opts.dist,
|
||||
};
|
||||
|
||||
// In development mode, the main process uses the @kbn/dev-cli-mode
|
||||
// bootstrap script instead of core's. The DevCliMode instance
|
||||
// is in charge of starting up the optimizer, and spawning another
|
||||
// `/script/kibana` process with the `isDevCliChild` varenv set to true.
|
||||
// This variable is then used to identify that we're the 'real'
|
||||
// Kibana server process, and will be using core's bootstrap script
|
||||
// to effectively start Kibana.
|
||||
const bootstrapScript = getBootstrapScript(cliArgs.dev);
|
||||
|
||||
await bootstrapScript({
|
||||
configs,
|
||||
cliArgs,
|
||||
applyConfigOverrides: (rawConfig) => applyConfigOverrides(rawConfig, opts, unknownOptions),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,18 +11,10 @@ import { CliArgs, Env, RawConfigService } from './config';
|
|||
import { Root } from './root';
|
||||
import { CriticalError } from './errors';
|
||||
|
||||
interface KibanaFeatures {
|
||||
// Indicates whether we can run Kibana in dev mode in which Kibana is run as
|
||||
// a child process together with optimizer "worker" processes that are
|
||||
// orchestrated by a parent process (dev mode only feature).
|
||||
isCliDevModeSupported: boolean;
|
||||
}
|
||||
|
||||
interface BootstrapArgs {
|
||||
configs: string[];
|
||||
cliArgs: CliArgs;
|
||||
applyConfigOverrides: (config: Record<string, any>) => Record<string, any>;
|
||||
features: KibanaFeatures;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,12 +22,7 @@ interface BootstrapArgs {
|
|||
* @internal
|
||||
* @param param0 - options
|
||||
*/
|
||||
export async function bootstrap({
|
||||
configs,
|
||||
cliArgs,
|
||||
applyConfigOverrides,
|
||||
features,
|
||||
}: BootstrapArgs) {
|
||||
export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: BootstrapArgs) {
|
||||
if (cliArgs.optimize) {
|
||||
// --optimize is deprecated and does nothing now, avoid starting up and just shutdown
|
||||
return;
|
||||
|
@ -52,7 +39,6 @@ export async function bootstrap({
|
|||
const env = Env.createDefault(REPO_ROOT, {
|
||||
configs,
|
||||
cliArgs,
|
||||
isDevCliParent: cliArgs.dev && features.isCliDevModeSupported && !process.env.isDevCliChild,
|
||||
});
|
||||
|
||||
const rawConfigService = new RawConfigService(env.configs, applyConfigOverrides);
|
||||
|
|
|
@ -6,26 +6,11 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const config = {
|
||||
path: 'dev',
|
||||
schema: schema.object({
|
||||
basePathProxyTarget: schema.number({
|
||||
defaultValue: 5603,
|
||||
}),
|
||||
}),
|
||||
// dev configuration is validated by the dev cli.
|
||||
// we only need to register the `dev` schema to avoid failing core's config validation
|
||||
schema: schema.object({}, { unknowns: 'ignore' }),
|
||||
};
|
||||
|
||||
export type DevConfigType = TypeOf<typeof config.schema>;
|
||||
|
||||
export class DevConfig {
|
||||
public basePathProxyTargetPort: number;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(rawConfig: DevConfigType) {
|
||||
this.basePathProxyTargetPort = rawConfig.basePathProxyTarget;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,5 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { config, DevConfig } from './dev_config';
|
||||
export type { DevConfigType } from './dev_config';
|
||||
export { config } from './dev_config';
|
||||
|
|
|
@ -11,7 +11,7 @@ jest.mock('fs', () => ({ readFileSync: mockReadFileSync }));
|
|||
|
||||
export const mockReadPkcs12Keystore = jest.fn();
|
||||
export const mockReadPkcs12Truststore = jest.fn();
|
||||
jest.mock('../utils', () => ({
|
||||
jest.mock('@kbn/crypto', () => ({
|
||||
readPkcs12Keystore: mockReadPkcs12Keystore,
|
||||
readPkcs12Truststore: mockReadPkcs12Truststore,
|
||||
}));
|
||||
|
|
|
@ -215,12 +215,12 @@ describe('throws when config is invalid', () => {
|
|||
beforeAll(() => {
|
||||
const realFs = jest.requireActual('fs');
|
||||
mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path));
|
||||
const utils = jest.requireActual('../utils');
|
||||
const crypto = jest.requireActual('@kbn/crypto');
|
||||
mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) =>
|
||||
utils.readPkcs12Keystore(path, password)
|
||||
crypto.readPkcs12Keystore(path, password)
|
||||
);
|
||||
mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) =>
|
||||
utils.readPkcs12Truststore(path, password)
|
||||
crypto.readPkcs12Truststore(path, password)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { readPkcs12Keystore, readPkcs12Truststore } from '@kbn/crypto';
|
||||
import { Duration } from 'moment';
|
||||
import { readFileSync } from 'fs';
|
||||
import { ConfigDeprecationProvider } from 'src/core/server';
|
||||
import { readPkcs12Keystore, readPkcs12Truststore } from '../utils';
|
||||
import { ServiceConfigDescriptor } from '../internal_types';
|
||||
|
||||
const hostURISchema = schema.uri({ scheme: ['http', 'https'] });
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createSHA256Hash } from '../utils';
|
||||
import { createSHA256Hash } from '@kbn/crypto';
|
||||
import { config } from './config';
|
||||
|
||||
const DEFAULT_CONFIG = Object.freeze(config.schema.validate({}));
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,12 +7,12 @@
|
|||
*/
|
||||
|
||||
import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema';
|
||||
import { IHttpConfig, SslConfig, sslSchema } from '@kbn/server-http-tools';
|
||||
import { hostname } from 'os';
|
||||
import url from 'url';
|
||||
|
||||
import { CspConfigType, CspConfig, ICspConfig } from '../csp';
|
||||
import { ExternalUrlConfig, IExternalUrlConfig } from '../external_url';
|
||||
import { SslConfig, sslSchema } from './ssl_config';
|
||||
|
||||
const validBasePathRegex = /^\/.*[^\/]$/;
|
||||
const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
@ -151,7 +151,7 @@ export const config = {
|
|||
};
|
||||
export type HttpConfigType = TypeOf<typeof config.schema>;
|
||||
|
||||
export class HttpConfig {
|
||||
export class HttpConfig implements IHttpConfig {
|
||||
public name: string;
|
||||
public autoListen: boolean;
|
||||
public host: string;
|
||||
|
|
|
@ -1288,6 +1288,30 @@ test('should return a stream in the body', async () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('closes sockets on timeout', async () => {
|
||||
const { registerRouter, server: innerServer } = await server.setup({
|
||||
...config,
|
||||
socketTimeout: 1000,
|
||||
});
|
||||
const router = new Router('', logger, enhanceWithContext);
|
||||
|
||||
router.get({ path: '/a', validate: false }, async (context, req, res) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
return res.ok({});
|
||||
});
|
||||
router.get({ path: '/b', validate: false }, (context, req, res) => res.ok({}));
|
||||
|
||||
registerRouter(router);
|
||||
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
expect(supertest(innerServer.listener).get('/a')).rejects.toThrow('socket hang up');
|
||||
|
||||
await supertest(innerServer.listener).get('/b').expect(200);
|
||||
});
|
||||
|
||||
describe('setup contract', () => {
|
||||
describe('#createSessionStorage', () => {
|
||||
test('creates session storage factory', async () => {
|
||||
|
|
|
@ -10,10 +10,15 @@ import { Server, Request } from '@hapi/hapi';
|
|||
import HapiStaticFiles from '@hapi/inert';
|
||||
import url from 'url';
|
||||
import uuid from 'uuid';
|
||||
import {
|
||||
createServer,
|
||||
getListenerOptions,
|
||||
getServerOptions,
|
||||
getRequestId,
|
||||
} from '@kbn/server-http-tools';
|
||||
|
||||
import { Logger, LoggerFactory } from '../logging';
|
||||
import { HttpConfig } from './http_config';
|
||||
import { createServer, getListenerOptions, getServerOptions, getRequestId } from './http_tools';
|
||||
import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
|
||||
import { adoptToHapiOnPreAuth, OnPreAuthHandler } from './lifecycle/on_pre_auth';
|
||||
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
|
||||
|
|
|
@ -242,29 +242,6 @@ test('returns http server contract on setup', async () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('does not start http server if process is dev cluster master', async () => {
|
||||
const configService = createConfigService();
|
||||
const httpServer = {
|
||||
isListening: () => false,
|
||||
setup: jest.fn().mockReturnValue({}),
|
||||
start: jest.fn(),
|
||||
stop: noop,
|
||||
};
|
||||
mockHttpServer.mockImplementation(() => httpServer);
|
||||
|
||||
const service = new HttpService({
|
||||
coreId,
|
||||
configService,
|
||||
env: Env.createDefault(REPO_ROOT, getEnvOptions({ isDevCliParent: true })),
|
||||
logger,
|
||||
});
|
||||
|
||||
await service.setup(setupDeps);
|
||||
await service.start();
|
||||
|
||||
expect(httpServer.start).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not start http server if configured with `autoListen:false`', async () => {
|
||||
const configService = createConfigService({
|
||||
autoListen: false,
|
||||
|
|
|
@ -153,15 +153,13 @@ export class HttpService
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Indicates if http server is configured to start listening on a configured port.
|
||||
* (if `server.autoListen` is not explicitly set to `false`.)
|
||||
*
|
||||
* @internal
|
||||
* */
|
||||
private shouldListen(config: HttpConfig) {
|
||||
return !this.coreContext.env.isDevCliParent && config.autoListen;
|
||||
return config.autoListen;
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
|
|
|
@ -1,274 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
jest.mock('fs', () => {
|
||||
const original = jest.requireActual('fs');
|
||||
return {
|
||||
// Hapi Inert patches native methods
|
||||
...original,
|
||||
readFileSync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'),
|
||||
}));
|
||||
|
||||
import supertest from 'supertest';
|
||||
import { Request, ResponseToolkit } from '@hapi/hapi';
|
||||
import Joi from 'joi';
|
||||
|
||||
import {
|
||||
defaultValidationErrorHandler,
|
||||
HapiValidationError,
|
||||
getServerOptions,
|
||||
getRequestId,
|
||||
} from './http_tools';
|
||||
import { HttpServer } from './http_server';
|
||||
import { HttpConfig, config } from './http_config';
|
||||
import { Router } from './router';
|
||||
import { loggingSystemMock } from '../logging/logging_system.mock';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
|
||||
const emptyOutput = {
|
||||
statusCode: 400,
|
||||
headers: {},
|
||||
payload: {
|
||||
statusCode: 400,
|
||||
error: '',
|
||||
validation: {
|
||||
source: '',
|
||||
keys: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('defaultValidationErrorHandler', () => {
|
||||
it('formats value validation errors correctly', () => {
|
||||
expect.assertions(1);
|
||||
const schema = Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
}).required()
|
||||
);
|
||||
|
||||
const error = schema.validate([{}], { abortEarly: false }).error as HapiValidationError;
|
||||
|
||||
// Emulate what Hapi v17 does by default
|
||||
error.output = { ...emptyOutput };
|
||||
error.output.payload.validation.keys = ['0.type', ''];
|
||||
|
||||
try {
|
||||
defaultValidationErrorHandler({} as Request, {} as ResponseToolkit, error);
|
||||
} catch (err) {
|
||||
// Verify the empty string gets corrected to 'value'
|
||||
expect(err.output.payload.validation.keys).toEqual(['0.type', 'value']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeouts', () => {
|
||||
const logger = loggingSystemMock.create();
|
||||
const server = new HttpServer(logger, 'foo');
|
||||
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
|
||||
|
||||
test('closes sockets on timeout', async () => {
|
||||
const router = new Router('', logger.get(), enhanceWithContext);
|
||||
router.get({ path: '/a', validate: false }, async (context, req, res) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
return res.ok({});
|
||||
});
|
||||
router.get({ path: '/b', validate: false }, (context, req, res) => res.ok({}));
|
||||
const { registerRouter, server: innerServer } = await server.setup({
|
||||
socketTimeout: 1000,
|
||||
host: '127.0.0.1',
|
||||
maxPayload: new ByteSizeValue(1024),
|
||||
ssl: {},
|
||||
cors: {
|
||||
enabled: false,
|
||||
},
|
||||
compression: { enabled: true },
|
||||
requestId: {
|
||||
allowFromAnyIp: true,
|
||||
ipAllowlist: [],
|
||||
},
|
||||
} as any);
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
expect(supertest(innerServer.listener).get('/a')).rejects.toThrow('socket hang up');
|
||||
|
||||
await supertest(innerServer.listener).get('/b').expect(200);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getServerOptions', () => {
|
||||
beforeEach(() =>
|
||||
jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`)
|
||||
);
|
||||
|
||||
it('properly configures TLS with default options', () => {
|
||||
const httpConfig = new HttpConfig(
|
||||
config.schema.validate({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
},
|
||||
}),
|
||||
{} as any,
|
||||
{} as any
|
||||
);
|
||||
|
||||
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": undefined,
|
||||
"cert": "content-some-certificate-path",
|
||||
"ciphers": "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA",
|
||||
"honorCipherOrder": true,
|
||||
"key": "content-some-key-path",
|
||||
"passphrase": undefined,
|
||||
"rejectUnauthorized": false,
|
||||
"requestCert": false,
|
||||
"secureOptions": 67108864,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('properly configures TLS with client authentication', () => {
|
||||
const httpConfig = new HttpConfig(
|
||||
config.schema.validate({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
certificateAuthorities: ['ca-1', 'ca-2'],
|
||||
clientAuthentication: 'required',
|
||||
},
|
||||
}),
|
||||
{} as any,
|
||||
{} as any
|
||||
);
|
||||
|
||||
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": Array [
|
||||
"content-ca-1",
|
||||
"content-ca-2",
|
||||
],
|
||||
"cert": "content-some-certificate-path",
|
||||
"ciphers": "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA",
|
||||
"honorCipherOrder": true,
|
||||
"key": "content-some-key-path",
|
||||
"passphrase": undefined,
|
||||
"rejectUnauthorized": true,
|
||||
"requestCert": true,
|
||||
"secureOptions": 67108864,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('properly configures CORS when cors enabled', () => {
|
||||
const httpConfig = new HttpConfig(
|
||||
config.schema.validate({
|
||||
cors: {
|
||||
enabled: true,
|
||||
allowCredentials: false,
|
||||
allowOrigin: ['*'],
|
||||
},
|
||||
}),
|
||||
{} as any,
|
||||
{} as any
|
||||
);
|
||||
|
||||
expect(getServerOptions(httpConfig).routes?.cors).toEqual({
|
||||
credentials: false,
|
||||
origin: ['*'],
|
||||
headers: ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRequestId', () => {
|
||||
describe('when allowFromAnyIp is true', () => {
|
||||
it('generates a UUID if no x-opaque-id header is present', () => {
|
||||
const request = {
|
||||
headers: {},
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual(
|
||||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses x-opaque-id header value if present', () => {
|
||||
const request = {
|
||||
headers: {
|
||||
'x-opaque-id': 'id from header',
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
},
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual(
|
||||
'id from header'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when allowFromAnyIp is false', () => {
|
||||
describe('and ipAllowlist is empty', () => {
|
||||
it('generates a UUID even if x-opaque-id header is present', () => {
|
||||
const request = {
|
||||
headers: { 'x-opaque-id': 'id from header' },
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: [] })).toEqual(
|
||||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and ipAllowlist is not empty', () => {
|
||||
it('uses x-opaque-id header if request comes from trusted IP address', () => {
|
||||
const request = {
|
||||
headers: { 'x-opaque-id': 'id from header' },
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual(
|
||||
'id from header'
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a UUID if request comes from untrusted IP address', () => {
|
||||
const request = {
|
||||
headers: { 'x-opaque-id': 'id from header' },
|
||||
raw: { req: { socket: { remoteAddress: '5.5.5.5' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual(
|
||||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
);
|
||||
});
|
||||
|
||||
it('generates UUID if request comes from trusted IP address but no x-opaque-id header is present', () => {
|
||||
const request = {
|
||||
headers: {},
|
||||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
|
||||
} as any;
|
||||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual(
|
||||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,186 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Server } from '@hapi/hapi';
|
||||
import type {
|
||||
Lifecycle,
|
||||
Request,
|
||||
ResponseToolkit,
|
||||
RouteOptionsCors,
|
||||
ServerOptions,
|
||||
Util,
|
||||
} from '@hapi/hapi';
|
||||
import Hoek from '@hapi/hoek';
|
||||
import type { ServerOptions as TLSOptions } from 'https';
|
||||
import type { ValidationError } from 'joi';
|
||||
import uuid from 'uuid';
|
||||
import { ensureNoUnsafeProperties } from '@kbn/std';
|
||||
import { HttpConfig } from './http_config';
|
||||
|
||||
const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'];
|
||||
/**
|
||||
* Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server.
|
||||
*/
|
||||
export function getServerOptions(config: HttpConfig, { configureTLS = true } = {}) {
|
||||
const cors: RouteOptionsCors | false = config.cors.enabled
|
||||
? {
|
||||
credentials: config.cors.allowCredentials,
|
||||
origin: config.cors.allowOrigin,
|
||||
headers: corsAllowedHeaders,
|
||||
}
|
||||
: false;
|
||||
// Note that all connection options configured here should be exactly the same
|
||||
// as in the legacy platform server (see `src/legacy/server/http/index`). Any change
|
||||
// SHOULD BE applied in both places. The only exception is TLS-specific options,
|
||||
// that are configured only here.
|
||||
const options: ServerOptions = {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
routes: {
|
||||
cache: {
|
||||
privacy: 'private',
|
||||
otherwise: 'private, no-cache, no-store, must-revalidate',
|
||||
},
|
||||
cors,
|
||||
payload: {
|
||||
maxBytes: config.maxPayload.getValueInBytes(),
|
||||
},
|
||||
validate: {
|
||||
failAction: defaultValidationErrorHandler,
|
||||
options: {
|
||||
abortEarly: false,
|
||||
},
|
||||
// TODO: This payload validation can be removed once the legacy platform is completely removed.
|
||||
// This is a default payload validation which applies to all LP routes which do not specify their own
|
||||
// `validate.payload` handler, in order to reduce the likelyhood of prototype pollution vulnerabilities.
|
||||
// (All NP routes are already required to specify their own validation in order to access the payload)
|
||||
payload: (value) => Promise.resolve(ensureNoUnsafeProperties(value)),
|
||||
},
|
||||
},
|
||||
state: {
|
||||
strictHeader: false,
|
||||
isHttpOnly: true,
|
||||
isSameSite: false, // necessary to allow using Kibana inside an iframe
|
||||
},
|
||||
};
|
||||
|
||||
if (configureTLS && config.ssl.enabled) {
|
||||
const ssl = config.ssl;
|
||||
|
||||
// TODO: Hapi types have a typo in `tls` property type definition: `https.RequestOptions` is used instead of
|
||||
// `https.ServerOptions`, and `honorCipherOrder` isn't presented in `https.RequestOptions`.
|
||||
const tlsOptions: TLSOptions = {
|
||||
ca: ssl.certificateAuthorities,
|
||||
cert: ssl.certificate,
|
||||
ciphers: config.ssl.cipherSuites.join(':'),
|
||||
// We use the server's cipher order rather than the client's to prevent the BEAST attack.
|
||||
honorCipherOrder: true,
|
||||
key: ssl.key,
|
||||
passphrase: ssl.keyPassphrase,
|
||||
secureOptions: ssl.getSecureOptions(),
|
||||
requestCert: ssl.requestCert,
|
||||
rejectUnauthorized: ssl.rejectUnauthorized,
|
||||
};
|
||||
|
||||
options.tls = tlsOptions;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function getListenerOptions(config: HttpConfig) {
|
||||
return {
|
||||
keepaliveTimeout: config.keepaliveTimeout,
|
||||
socketTimeout: config.socketTimeout,
|
||||
};
|
||||
}
|
||||
|
||||
interface ListenerOptions {
|
||||
keepaliveTimeout: number;
|
||||
socketTimeout: number;
|
||||
}
|
||||
|
||||
export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) {
|
||||
const server = new Server(serverOptions);
|
||||
|
||||
server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout;
|
||||
server.listener.setTimeout(listenerOptions.socketTimeout);
|
||||
server.listener.on('timeout', (socket) => {
|
||||
socket.destroy();
|
||||
});
|
||||
server.listener.on('clientError', (err, socket) => {
|
||||
if (socket.writable) {
|
||||
socket.end(Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii'));
|
||||
} else {
|
||||
socket.destroy(err);
|
||||
}
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapi extends the ValidationError interface to add this output key with more data.
|
||||
*/
|
||||
export interface HapiValidationError extends ValidationError {
|
||||
output: {
|
||||
statusCode: number;
|
||||
headers: Util.Dictionary<string | string[]>;
|
||||
payload: {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message?: string;
|
||||
validation: {
|
||||
source: string;
|
||||
keys: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to replicate Hapi v16 and below's validation responses. Should be used in the routes.validate.failAction key.
|
||||
*/
|
||||
export function defaultValidationErrorHandler(
|
||||
request: Request,
|
||||
h: ResponseToolkit,
|
||||
err?: Error
|
||||
): Lifecycle.ReturnValue {
|
||||
// Newer versions of Joi don't format the key for missing params the same way. This shim
|
||||
// provides backwards compatibility. Unfortunately, Joi doesn't export it's own Error class
|
||||
// in JS so we have to rely on the `name` key before we can cast it.
|
||||
//
|
||||
// The Hapi code we're 'overwriting' can be found here:
|
||||
// https://github.com/hapijs/hapi/blob/master/lib/validation.js#L102
|
||||
if (err && err.name === 'ValidationError' && err.hasOwnProperty('output')) {
|
||||
const validationError: HapiValidationError = err as HapiValidationError;
|
||||
const validationKeys: string[] = [];
|
||||
|
||||
validationError.details.forEach((detail) => {
|
||||
if (detail.path.length > 0) {
|
||||
validationKeys.push(Hoek.escapeHtml(detail.path.join('.')));
|
||||
} else {
|
||||
// If no path, use the value sigil to signal the entire value had an issue.
|
||||
validationKeys.push('value');
|
||||
}
|
||||
});
|
||||
|
||||
validationError.output.payload.validation.keys = validationKeys;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
export function getRequestId(request: Request, options: HttpConfig['requestId']): string {
|
||||
return options.allowFromAnyIp ||
|
||||
// socket may be undefined in integration tests that connect via the http listener directly
|
||||
(request.raw.req.socket?.remoteAddress &&
|
||||
options.ipAllowlist.includes(request.raw.req.socket.remoteAddress))
|
||||
? request.headers['x-opaque-id'] ?? uuid.v4()
|
||||
: uuid.v4();
|
||||
}
|
|
@ -8,10 +8,10 @@
|
|||
|
||||
import { Request, ResponseToolkit, Server } from '@hapi/hapi';
|
||||
import { format as formatUrl } from 'url';
|
||||
import { createServer, getListenerOptions, getServerOptions } from '@kbn/server-http-tools';
|
||||
|
||||
import { Logger } from '../logging';
|
||||
import { HttpConfig } from './http_config';
|
||||
import { createServer, getListenerOptions, getServerOptions } from './http_tools';
|
||||
|
||||
export class HttpsRedirectServer {
|
||||
private server?: Server;
|
||||
|
|
|
@ -56,7 +56,6 @@ export type {
|
|||
DestructiveRouteMethod,
|
||||
SafeRouteMethod,
|
||||
} from './router';
|
||||
export { BasePathProxyServer } from './base_path_proxy_server';
|
||||
export type { OnPreRoutingHandler, OnPreRoutingToolkit } from './lifecycle/on_pre_routing';
|
||||
export type {
|
||||
AuthenticationHandler,
|
||||
|
|
|
@ -11,14 +11,12 @@ import Boom from '@hapi/boom';
|
|||
import supertest from 'supertest';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { HttpService } from '../http_service';
|
||||
|
||||
import { contextServiceMock } from '../../context/context_service.mock';
|
||||
import { loggingSystemMock } from '../../logging/logging_system.mock';
|
||||
import { createHttpServer } from '../test_utils';
|
||||
import { HttpService } from '../http_service';
|
||||
|
||||
let server: HttpService;
|
||||
|
||||
let logger: ReturnType<typeof loggingSystemMock.create>;
|
||||
const contextSetup = contextServiceMock.createSetupContract();
|
||||
|
||||
|
@ -28,7 +26,6 @@ const setupDeps = {
|
|||
|
||||
beforeEach(() => {
|
||||
logger = loggingSystemMock.create();
|
||||
|
||||
server = createHttpServer({ logger });
|
||||
});
|
||||
|
||||
|
|
|
@ -7,16 +7,12 @@
|
|||
*/
|
||||
|
||||
jest.mock('../../../legacy/server/kbn_server');
|
||||
jest.mock('./cli_dev_mode');
|
||||
|
||||
import { BehaviorSubject, throwError } from 'rxjs';
|
||||
import { REPO_ROOT } from '@kbn/dev-utils';
|
||||
|
||||
// @ts-expect-error js file to remove TS dependency on cli
|
||||
import { CliDevMode as MockCliDevMode } from './cli_dev_mode';
|
||||
import KbnServer from '../../../legacy/server/kbn_server';
|
||||
import { Config, Env, ObjectToConfigAdapter } from '../config';
|
||||
import { BasePathProxyServer } from '../http';
|
||||
import { DiscoveredPlugin } from '../plugins';
|
||||
|
||||
import { getEnvOptions, configServiceMock } from '../config/mocks';
|
||||
|
@ -228,7 +224,6 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
);
|
||||
|
||||
expect(MockKbnServer).not.toHaveBeenCalled();
|
||||
expect(MockCliDevMode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('reconfigures logging configuration if new config is received.', async () => {
|
||||
|
@ -335,74 +330,6 @@ describe('once LegacyService is set up without connection info', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('once LegacyService is set up in `devClusterMaster` mode', () => {
|
||||
beforeEach(() => {
|
||||
configService.atPath.mockImplementation((path) => {
|
||||
return new BehaviorSubject(
|
||||
path === 'dev' ? { basePathProxyTargetPort: 100500 } : { basePath: '/abc' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('creates CliDevMode without base path proxy.', async () => {
|
||||
const devClusterLegacyService = new LegacyService({
|
||||
coreId,
|
||||
env: Env.createDefault(
|
||||
REPO_ROOT,
|
||||
getEnvOptions({
|
||||
cliArgs: { silent: true, basePath: false },
|
||||
isDevCliParent: true,
|
||||
})
|
||||
),
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
|
||||
await devClusterLegacyService.setupLegacyConfig();
|
||||
await devClusterLegacyService.setup(setupDeps);
|
||||
await devClusterLegacyService.start(startDeps);
|
||||
|
||||
expect(MockCliDevMode.fromCoreServices).toHaveBeenCalledTimes(1);
|
||||
expect(MockCliDevMode.fromCoreServices).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ silent: true, basePath: false }),
|
||||
expect.objectContaining({
|
||||
get: expect.any(Function),
|
||||
set: expect.any(Function),
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test('creates CliDevMode with base path proxy.', async () => {
|
||||
const devClusterLegacyService = new LegacyService({
|
||||
coreId,
|
||||
env: Env.createDefault(
|
||||
REPO_ROOT,
|
||||
getEnvOptions({
|
||||
cliArgs: { quiet: true, basePath: true },
|
||||
isDevCliParent: true,
|
||||
})
|
||||
),
|
||||
logger,
|
||||
configService: configService as any,
|
||||
});
|
||||
|
||||
await devClusterLegacyService.setupLegacyConfig();
|
||||
await devClusterLegacyService.setup(setupDeps);
|
||||
await devClusterLegacyService.start(startDeps);
|
||||
|
||||
expect(MockCliDevMode.fromCoreServices).toHaveBeenCalledTimes(1);
|
||||
expect(MockCliDevMode.fromCoreServices).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ quiet: true, basePath: true }),
|
||||
expect.objectContaining({
|
||||
get: expect.any(Function),
|
||||
set: expect.any(Function),
|
||||
}),
|
||||
expect.any(BasePathProxyServer)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
test('Cannot start without setup phase', async () => {
|
||||
const legacyService = new LegacyService({
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs';
|
||||
import { combineLatest, ConnectableObservable, Observable, Subscription } from 'rxjs';
|
||||
import { first, map, publishReplay, tap } from 'rxjs/operators';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { PathConfigType } from '@kbn/utils';
|
||||
|
@ -18,9 +18,7 @@ import { CoreService } from '../../types';
|
|||
import { Config } from '../config';
|
||||
import { CoreContext } from '../core_context';
|
||||
import { CspConfigType, config as cspConfig } from '../csp';
|
||||
import { DevConfig, DevConfigType, config as devConfig } from '../dev';
|
||||
import {
|
||||
BasePathProxyServer,
|
||||
HttpConfig,
|
||||
HttpConfigType,
|
||||
config as httpConfig,
|
||||
|
@ -64,7 +62,6 @@ export class LegacyService implements CoreService {
|
|||
/** Symbol to represent the legacy platform as a fake "plugin". Used by the ContextService */
|
||||
public readonly legacyId = Symbol();
|
||||
private readonly log: Logger;
|
||||
private readonly devConfig$: Observable<DevConfig>;
|
||||
private readonly httpConfig$: Observable<HttpConfig>;
|
||||
private kbnServer?: LegacyKbnServer;
|
||||
private configSubscription?: Subscription;
|
||||
|
@ -77,9 +74,6 @@ export class LegacyService implements CoreService {
|
|||
const { logger, configService } = coreContext;
|
||||
|
||||
this.log = logger.get('legacy-service');
|
||||
this.devConfig$ = configService
|
||||
.atPath<DevConfigType>(devConfig.path)
|
||||
.pipe(map((rawConfig) => new DevConfig(rawConfig)));
|
||||
this.httpConfig$ = combineLatest(
|
||||
configService.atPath<HttpConfigType>(httpConfig.path),
|
||||
configService.atPath<CspConfigType>(cspConfig.path),
|
||||
|
@ -142,17 +136,12 @@ export class LegacyService implements CoreService {
|
|||
|
||||
this.log.debug('starting legacy service');
|
||||
|
||||
// Receive initial config and create kbnServer/ClusterManager.
|
||||
if (this.coreContext.env.isDevCliParent) {
|
||||
await this.setupCliDevMode(this.legacyRawConfig!);
|
||||
} else {
|
||||
this.kbnServer = await this.createKbnServer(
|
||||
this.settings!,
|
||||
this.legacyRawConfig!,
|
||||
setupDeps,
|
||||
startDeps
|
||||
);
|
||||
}
|
||||
this.kbnServer = await this.createKbnServer(
|
||||
this.settings!,
|
||||
this.legacyRawConfig!,
|
||||
setupDeps,
|
||||
startDeps
|
||||
);
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
|
@ -169,26 +158,6 @@ export class LegacyService implements CoreService {
|
|||
}
|
||||
}
|
||||
|
||||
private async setupCliDevMode(config: LegacyConfig) {
|
||||
const basePathProxy$ = this.coreContext.env.cliArgs.basePath
|
||||
? combineLatest([this.devConfig$, this.httpConfig$]).pipe(
|
||||
first(),
|
||||
map(
|
||||
([dev, http]) =>
|
||||
new BasePathProxyServer(this.coreContext.logger.get('server'), http, dev)
|
||||
)
|
||||
)
|
||||
: EMPTY;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { CliDevMode } = require('./cli_dev_mode');
|
||||
CliDevMode.fromCoreServices(
|
||||
this.coreContext.env.cliArgs,
|
||||
config,
|
||||
await basePathProxy$.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
private async createKbnServer(
|
||||
settings: LegacyVars,
|
||||
config: LegacyConfig,
|
||||
|
|
|
@ -91,7 +91,7 @@ const createPlugin = (
|
|||
});
|
||||
};
|
||||
|
||||
async function testSetup(options: { isDevCliParent?: boolean } = {}) {
|
||||
async function testSetup() {
|
||||
mockPackage.raw = {
|
||||
branch: 'feature-v1',
|
||||
version: 'v1',
|
||||
|
@ -103,10 +103,7 @@ async function testSetup(options: { isDevCliParent?: boolean } = {}) {
|
|||
};
|
||||
|
||||
coreId = Symbol('core');
|
||||
env = Env.createDefault(REPO_ROOT, {
|
||||
...getEnvOptions(),
|
||||
isDevCliParent: options.isDevCliParent ?? false,
|
||||
});
|
||||
env = Env.createDefault(REPO_ROOT, getEnvOptions());
|
||||
|
||||
config$ = new BehaviorSubject<Record<string, any>>({ plugins: { initialize: true } });
|
||||
const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ });
|
||||
|
@ -626,30 +623,3 @@ describe('PluginsService', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PluginService when isDevCliParent is true', () => {
|
||||
beforeEach(async () => {
|
||||
await testSetup({
|
||||
isDevCliParent: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('#discover()', () => {
|
||||
it('does not try to run discovery', async () => {
|
||||
await expect(pluginsService.discover({ environment: environmentSetup })).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"pluginPaths": Array [],
|
||||
"pluginTree": undefined,
|
||||
"uiPlugins": Object {
|
||||
"browserConfigs": Map {},
|
||||
"internal": Map {},
|
||||
"public": Map {},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(mockDiscover).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import Path from 'path';
|
||||
import { Observable, EMPTY } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators';
|
||||
import { pick } from '@kbn/std';
|
||||
|
||||
|
@ -75,11 +75,9 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
private readonly config$: Observable<PluginsConfig>;
|
||||
private readonly pluginConfigDescriptors = new Map<PluginName, PluginConfigDescriptor>();
|
||||
private readonly uiPluginInternalInfo = new Map<PluginName, InternalPluginInfo>();
|
||||
private readonly discoveryDisabled: boolean;
|
||||
|
||||
constructor(private readonly coreContext: CoreContext) {
|
||||
this.log = coreContext.logger.get('plugins-service');
|
||||
this.discoveryDisabled = coreContext.env.isDevCliParent;
|
||||
this.pluginsSystem = new PluginsSystem(coreContext);
|
||||
this.configService = coreContext.configService;
|
||||
this.config$ = coreContext.configService
|
||||
|
@ -90,14 +88,9 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
public async discover({ environment }: PluginsServiceDiscoverDeps) {
|
||||
const config = await this.config$.pipe(first()).toPromise();
|
||||
|
||||
const { error$, plugin$ } = this.discoveryDisabled
|
||||
? {
|
||||
error$: EMPTY,
|
||||
plugin$: EMPTY,
|
||||
}
|
||||
: discover(config, this.coreContext, {
|
||||
uuid: environment.instanceUuid,
|
||||
});
|
||||
const { error$, plugin$ } = discover(config, this.coreContext, {
|
||||
uuid: environment.instanceUuid,
|
||||
});
|
||||
|
||||
await this.handleDiscoveryErrors(error$);
|
||||
await this.handleDiscoveredPlugins(plugin$);
|
||||
|
@ -122,8 +115,7 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
const config = await this.config$.pipe(first()).toPromise();
|
||||
|
||||
let contracts = new Map<PluginName, unknown>();
|
||||
const initialize = config.initialize && !this.coreContext.env.isDevCliParent;
|
||||
if (initialize) {
|
||||
if (config.initialize) {
|
||||
contracts = await this.pluginsSystem.setupPlugins(deps);
|
||||
this.registerPluginStaticDirs(deps);
|
||||
} else {
|
||||
|
@ -131,7 +123,7 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
}
|
||||
|
||||
return {
|
||||
initialized: initialize,
|
||||
initialized: config.initialize,
|
||||
contracts,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ConnectableObservable, Subscription, of } from 'rxjs';
|
||||
import { ConnectableObservable, Subscription } from 'rxjs';
|
||||
import { first, publishReplay, switchMap, concatMap, tap } from 'rxjs/operators';
|
||||
|
||||
import { Env, RawConfigurationProvider } from '../config';
|
||||
|
@ -25,7 +25,7 @@ export class Root {
|
|||
|
||||
constructor(
|
||||
rawConfigProvider: RawConfigurationProvider,
|
||||
private readonly env: Env,
|
||||
env: Env,
|
||||
private readonly onShutdown?: (reason?: Error | string) => void
|
||||
) {
|
||||
this.loggingSystem = new LoggingSystem();
|
||||
|
@ -87,10 +87,7 @@ export class Root {
|
|||
// Stream that maps config updates to logger updates, including update failures.
|
||||
const update$ = configService.getConfig$().pipe(
|
||||
// always read the logging config when the underlying config object is re-read
|
||||
// except for the CLI process where we only apply the default logging config once
|
||||
switchMap(() =>
|
||||
this.env.isDevCliParent ? of(undefined) : configService.atPath<LoggingConfigType>('logging')
|
||||
),
|
||||
switchMap(() => configService.atPath<LoggingConfigType>('logging')),
|
||||
concatMap((config) => this.loggingSystem.upgrade(config)),
|
||||
// This specifically console.logs because we were not able to configure the logger.
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue