Remove core->cli dependency (#95145) (#95775)

* 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:
Pierre Gayvallet 2021-03-30 15:48:48 +02:00 committed by GitHub
parent 16b8c0213a
commit 41eb6e2b12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 1608 additions and 1969 deletions

View file

@ -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*',

View file

@ -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>;

View file

@ -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",

View file

@ -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.

View 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'],
};

View 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"
}
}

View 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/');
});
});
});

View file

@ -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;

View 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();
}

View file

@ -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();

View file

@ -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,
});
}

View 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;
}
}

View 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);
}
}

View 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';

View 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),
};
};

View 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;
}
}

View 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;
}

View file

@ -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)([\\\/]|$)/,

View file

@ -6,5 +6,4 @@
* Side Public License, v 1.
*/
export * from './cli_dev_mode';
export * from './log';
export { bootstrapDevMode } from './bootstrap';

View 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;
};

View file

@ -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,
},

View file

@ -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');

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"outDir": "./target",
"declarationMap": true,
"types": ["jest", "node"]
},
"include": ["./src/**/*.ts"],
"exclude": ["target"]
}

View file

@ -30,6 +30,5 @@ export function getEnvOptions(options: DeepPartial<EnvOptions> = {}): EnvOptions
runExamples: false,
...(options.cliArgs || {}),
},
isDevCliParent: options.isDevCliParent !== undefined ? options.isDevCliParent : false,
};
}

View file

@ -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,

View file

@ -36,7 +36,6 @@ test('correctly creates default environment in dev mode.', () => {
REPO_ROOT,
getEnvOptions({
configs: ['/test/cwd/config/kibana.yml'],
isDevCliParent: true,
})
);

View file

@ -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>({

View file

@ -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';

View file

@ -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';

View file

@ -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$'>;

View file

@ -0,0 +1,3 @@
# @kbn/crypto
Crypto tools and utilities for Kibana

View 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'],
};

View 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"
}
}

View file

@ -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

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target",
"declaration": true,
"declarationMap": true
},
"include": [
"src/**/*"
]
}

View file

@ -0,0 +1,3 @@
# @kbn/http-tools
Http utilities for core and the basepath server

View 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'],
};

View 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"
}
}

View 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;
}

View file

@ -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']);
}
});
});

View file

@ -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;
}

View 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,
};
}

View 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'
);
});
});
});
});

View 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();
}

View 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'],
});
});
});

View 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;
}

View 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';

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { CliDevMode } from '../../../dev/cli_dev_mode';
export { SslConfig, sslSchema } from './ssl_config';

View file

@ -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,
}));

View file

@ -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)
);

View file

@ -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');

View 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;
}

View file

@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target",
"declaration": true,
"declarationMap": true
},
"include": [
"src/**/*"
],
"dependencies": {
"@kbn/std": "link:../kbn-std"
}
}

View file

@ -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);

View file

@ -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),
});
});

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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';

View file

@ -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,
}));

View file

@ -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)
);
});

View file

@ -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'] });

View file

@ -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

View file

@ -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;

View file

@ -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 () => {

View file

@ -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';

View file

@ -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,

View file

@ -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() {

View file

@ -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'
);
});
});
});
});

View file

@ -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();
}

View file

@ -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;

View file

@ -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,

View file

@ -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 });
});

View file

@ -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({

View file

@ -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,

View file

@ -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();
});
});
});

View file

@ -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,
};
}

View file

@ -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