[7.x] Remove /src/legacy (#95510) (#96283)

* Remove /src/legacy (#95510)

* starting removing stuff

* fix jest config

* disable CLI mode until other PR is merged

* fix the schema

* add deprecation for maxPayloadBytes

* fix legacy start logic

* deletes `env` from unknown args

* fix FTR test config

* some legacy service deletion

* move config validation

* remove legacy exports from entrypoint

* preserve legacy logging in core logging config

* try to fix uiSettings integration tests

* fix legacy service tests

* more type fix

* use fromRoot from @kbn/utils

* cleanup kibana.d.ts

* fix unit tests

* remove src/core/server/utils

* fix server script

* add integration test for `/{path*}` route

* add unit tests on legacy config

* adapt uiSetting IT bis

* fix tests

* update generated doc

* address some review comments

* move review comments

* fix some stuff

* fix some stuff

* fix some stuff

* fix some stuff bis

* generated doc

* add test for ensureValidConfiguration
# Conflicts:
#	.github/CODEOWNERS
#	src/cli_plugin/install/core_plugins/kibana/public/context/query_parameters/state.js
#	src/core/server/http/http_config.ts
#	x-pack/test/functional/config.js

* codestyle
This commit is contained in:
Pierre Gayvallet 2021-04-06 12:08:31 +02:00 committed by GitHub
parent ad7866592e
commit 248390f779
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
111 changed files with 743 additions and 4096 deletions

View file

@ -21,19 +21,13 @@ snapshots.js
# plugin overrides
/src/core/lib/kbn_internal_native_observable
/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken
/src/plugins/data/common/es_query/kuery/ast/_generated_/**
/src/plugins/vis_type_timelion/common/_generated_/**
/x-pack/legacy/plugins/**/__tests__/fixtures/**
/x-pack/plugins/apm/e2e/tmp/*
/x-pack/plugins/canvas/canvas_plugin
/x-pack/plugins/canvas/shareable_runtime/build
/x-pack/plugins/canvas/storybook/build
/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/**
/x-pack/legacy/plugins/infra/common/graphql/types.ts
/x-pack/legacy/plugins/infra/public/graphql/types.ts
/x-pack/legacy/plugins/infra/server/graphql/types.ts
/x-pack/legacy/plugins/maps/public/vendor/**
# package overrides
/packages/elastic-eslint-config-kibana

View file

@ -416,11 +416,7 @@ module.exports = {
errorMessage: `Common code can not import from server or public, use a common directory.`,
},
{
target: [
'src/legacy/**/*',
'(src|x-pack)/plugins/**/(public|server)/**/*',
'examples/**/*',
],
target: ['(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*'],
from: [
'src/core/public/**/*',
'!src/core/public/index.ts', // relative import
@ -434,8 +430,6 @@ module.exports = {
'!src/core/server/mocks{,.ts}',
'!src/core/server/types{,.ts}',
'!src/core/server/test_utils{,.ts}',
'!src/core/server/utils', // ts alias
'!src/core/server/utils/**/*',
// for absolute imports until fixed in
// https://github.com/elastic/kibana/issues/36096
'!src/core/server/*.test.mocks{,.ts}',
@ -448,7 +442,6 @@ module.exports = {
},
{
target: [
'src/legacy/**/*',
'(src|x-pack)/plugins/**/(public|server)/**/*',
'examples/**/*',
'!(src|x-pack)/**/*.test.*',
@ -486,7 +479,7 @@ module.exports = {
},
{
target: ['src/core/**/*'],
from: ['plugins/**/*', 'src/plugins/**/*', 'src/legacy/ui/**/*'],
from: ['plugins/**/*', 'src/plugins/**/*'],
errorMessage: 'The core cannot depend on any plugins.',
},
{
@ -494,19 +487,6 @@ module.exports = {
from: ['ui/**/*'],
errorMessage: 'Plugins cannot import legacy UI code.',
},
{
from: ['src/legacy/ui/**/*', 'ui/**/*'],
target: [
'test/plugin_functional/plugins/**/public/np_ready/**/*',
'test/plugin_functional/plugins/**/server/np_ready/**/*',
],
allowSameFolder: true,
errorMessage:
'NP-ready code should not import from /src/legacy/ui/** folder. ' +
'Instead of importing from /src/legacy/ui/** deeply within a np_ready folder, ' +
'import those things once at the top level of your plugin and pass those down, just ' +
'like you pass down `core` and `plugins` objects.',
},
],
},
],

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> | Error | Buffer | {
custom: <T extends string | Record<string, any> | Error | Buffer | Stream | {
message: string | Error;
attributes?: Record<string, any> | undefined;
} | Stream | undefined>(options: CustomHttpResponseOptions<T>) => KibanaResponse<T>;
} | undefined>(options: CustomHttpResponseOptions<T>) => KibanaResponse<T>;
badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse<ResponseError>;
unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse<ResponseError>;
forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse<ResponseError>;

View file

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

View file

@ -1,24 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md)
## LegacyServiceSetupDeps interface
> Warning: This API is now obsolete.
>
>
<b>Signature:</b>
```typescript
export interface LegacyServiceSetupDeps
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) | <code>LegacyCoreSetup</code> | |
| [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) | <code>Record&lt;string, unknown&gt;</code> | |
| [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) | <code>UiPlugins</code> | |

View file

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

View file

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

View file

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

View file

@ -1,23 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md)
## LegacyServiceStartDeps interface
> Warning: This API is now obsolete.
>
>
<b>Signature:</b>
```typescript
export interface LegacyServiceStartDeps
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-core-server.legacyservicestartdeps.core.md) | <code>LegacyCoreStart</code> | |
| [plugins](./kibana-plugin-core-server.legacyservicestartdeps.plugins.md) | <code>Record&lt;string, unknown&gt;</code> | |

View file

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

View file

@ -110,8 +110,6 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. |
| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @<!-- -->deprecated. The new elasticsearch client doesn't wrap errors anymore. |
| [LegacyRequest](./kibana-plugin-core-server.legacyrequest.md) | |
| [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) | |
| [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) | |
| [LoggerContextConfigInput](./kibana-plugin-core-server.loggercontextconfiginput.md) | |
| [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. |
| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. |

View file

@ -12,7 +12,7 @@ start(core: CoreStart): {
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
};
indexPatterns: {
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "update" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import(".").IndexPatternsService>;
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import(".").IndexPatternsService>;
};
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
};
@ -31,7 +31,7 @@ start(core: CoreStart): {
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
};
indexPatterns: {
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "update" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import(".").IndexPatternsService>;
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "get" | "delete" | "create" | "bulkCreate" | "checkConflicts" | "find" | "bulkGet" | "resolve" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo" | "openPointInTimeForType" | "closePointInTime" | "createPointInTimeFinder" | "errors">, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise<import(".").IndexPatternsService>;
};
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
}`

View file

@ -12,7 +12,6 @@ module.exports = {
projects: [
'<rootDir>/packages/*/jest.config.js',
'<rootDir>/src/*/jest.config.js',
'<rootDir>/src/legacy/*/jest.config.js',
'<rootDir>/src/plugins/*/jest.config.js',
'<rootDir>/test/*/jest.config.js',
'<rootDir>/x-pack/plugins/*/jest.config.js',

15
kibana.d.ts vendored
View file

@ -13,18 +13,3 @@ import * as Public from 'src/core/public';
import * as Server from 'src/core/server';
export { Public, Server };
/**
* All exports from TS ambient definitions (where types are added for JS source in a .d.ts file).
*/
import * as LegacyKibanaServer from './src/legacy/server/kbn_server';
/**
* Re-export legacy types under a namespace.
*/
export namespace Legacy {
export type KibanaConfig = LegacyKibanaServer.KibanaConfig;
export type Request = LegacyKibanaServer.Request;
export type ResponseToolkit = LegacyKibanaServer.ResponseToolkit;
export type Server = LegacyKibanaServer.Server;
}

View file

@ -27,8 +27,6 @@ it('produces the right watch and ignore list', () => {
expect(watchPaths).toMatchInlineSnapshot(`
Array [
<absolute path>/src/core,
<absolute path>/src/legacy/server,
<absolute path>/src/legacy/utils,
<absolute path>/config,
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test,
<absolute path>/src/plugins,

View file

@ -1,69 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#get correctly handles server config.: default 1`] = `
Object {
"autoListen": true,
"basePath": "/abc",
"compression": Object {
"enabled": true,
},
"cors": false,
"customResponseHeaders": Object {
"custom-header": "custom-value",
},
"host": "host",
"keepaliveTimeout": 5000,
"maxPayload": 1000,
"name": "kibana-hostname",
"port": 1234,
"publicBaseUrl": "https://myhost.com/abc",
"rewriteBasePath": false,
"socketTimeout": 2000,
"ssl": Object {
"enabled": true,
"keyPassphrase": "some-phrase",
"someNewValue": "new",
},
"uuid": undefined,
"xsrf": Object {
"allowlist": Array [],
"disableProtection": false,
},
}
`;
exports[`#get correctly handles server config.: disabled ssl 1`] = `
Object {
"autoListen": true,
"basePath": "/abc",
"compression": Object {
"enabled": true,
},
"cors": false,
"customResponseHeaders": Object {
"custom-header": "custom-value",
},
"host": "host",
"keepaliveTimeout": 5000,
"maxPayload": 1000,
"name": "kibana-hostname",
"port": 1234,
"publicBaseUrl": "http://myhost.com/abc",
"rewriteBasePath": false,
"socketTimeout": 2000,
"ssl": Object {
"certificate": "cert",
"enabled": false,
"key": "key",
},
"uuid": undefined,
"xsrf": Object {
"allowlist": Array [],
"disableProtection": false,
},
}
`;
exports[`#get correctly handles silent logging config. 1`] = `
Object {
"appenders": Object {
@ -78,6 +14,7 @@ Object {
"root": Object {
"level": "off",
},
"silent": true,
}
`;
@ -93,10 +30,13 @@ Object {
"type": "legacy-appender",
},
},
"dest": "/some/path.log",
"json": true,
"loggers": undefined,
"root": Object {
"level": "all",
},
"verbose": true,
}
`;

View file

@ -65,59 +65,6 @@ describe('#get', () => {
expect(configAdapter.get('logging')).toMatchSnapshot();
});
test('correctly handles server config.', () => {
const configAdapter = new LegacyObjectToConfigAdapter({
server: {
name: 'kibana-hostname',
autoListen: true,
basePath: '/abc',
cors: false,
customResponseHeaders: { 'custom-header': 'custom-value' },
host: 'host',
maxPayloadBytes: 1000,
keepaliveTimeout: 5000,
socketTimeout: 2000,
port: 1234,
publicBaseUrl: 'https://myhost.com/abc',
rewriteBasePath: false,
ssl: { enabled: true, keyPassphrase: 'some-phrase', someNewValue: 'new' },
compression: { enabled: true },
someNotSupportedValue: 'val',
xsrf: {
disableProtection: false,
allowlist: [],
},
},
});
const configAdapterWithDisabledSSL = new LegacyObjectToConfigAdapter({
server: {
name: 'kibana-hostname',
autoListen: true,
basePath: '/abc',
cors: false,
customResponseHeaders: { 'custom-header': 'custom-value' },
host: 'host',
maxPayloadBytes: 1000,
keepaliveTimeout: 5000,
socketTimeout: 2000,
port: 1234,
publicBaseUrl: 'http://myhost.com/abc',
rewriteBasePath: false,
ssl: { enabled: false, certificate: 'cert', key: 'key' },
compression: { enabled: true },
someNotSupportedValue: 'val',
xsrf: {
disableProtection: false,
allowlist: [],
},
},
});
expect(configAdapter.get('server')).toMatchSnapshot('default');
expect(configAdapterWithDisabledSSL.get('server')).toMatchSnapshot('disabled ssl');
});
});
describe('#set', () => {

View file

@ -9,15 +9,6 @@
import { ConfigPath } from '../config';
import { ObjectToConfigAdapter } from '../object_to_config_adapter';
// TODO: fix once core schemas are moved to this package
type LoggingConfigType = any;
/**
* @internal
* @deprecated
*/
export type LegacyVars = Record<string, any>;
/**
* Represents logging config supported by the legacy platform.
*/
@ -30,7 +21,7 @@ export interface LegacyLoggingConfig {
events?: Record<string, string>;
}
type MixedLoggingConfig = LegacyLoggingConfig & Partial<LoggingConfigType>;
type MixedLoggingConfig = LegacyLoggingConfig & Record<string, any>;
/**
* Represents adapter between config provided by legacy platform and `Config`
@ -48,6 +39,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
},
root: { level: 'info', ...root },
loggers,
...legacyLoggingConfig,
};
if (configValue.silent) {
@ -61,47 +53,11 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
return loggingConfig;
}
private static transformServer(configValue: any = {}) {
// TODO: New platform uses just a subset of `server` config from the legacy platform,
// new values will be exposed once we need them
return {
autoListen: configValue.autoListen,
basePath: configValue.basePath,
cors: configValue.cors,
customResponseHeaders: configValue.customResponseHeaders,
host: configValue.host,
maxPayload: configValue.maxPayloadBytes,
name: configValue.name,
port: configValue.port,
publicBaseUrl: configValue.publicBaseUrl,
rewriteBasePath: configValue.rewriteBasePath,
ssl: configValue.ssl,
keepaliveTimeout: configValue.keepaliveTimeout,
socketTimeout: configValue.socketTimeout,
compression: configValue.compression,
uuid: configValue.uuid,
xsrf: configValue.xsrf,
};
}
private static transformPlugins(configValue: LegacyVars = {}) {
// These properties are the only ones we use from the existing `plugins` config node
// since `scanDirs` isn't respected by new platform plugin discovery.
return {
initialize: configValue.initialize,
paths: configValue.paths,
};
}
public get(configPath: ConfigPath) {
const configValue = super.get(configPath);
switch (configPath) {
case 'logging':
return LegacyObjectToConfigAdapter.transformLogging(configValue as LegacyLoggingConfig);
case 'server':
return LegacyObjectToConfigAdapter.transformServer(configValue);
case 'plugins':
return LegacyObjectToConfigAdapter.transformPlugins(configValue as LegacyVars);
default:
return configValue;
}

View file

@ -11,6 +11,7 @@
"kbn:watch": "yarn build --watch"
},
"dependencies": {
"@kbn/utils": "link:../kbn-utils"
"@kbn/utils": "link:../kbn-utils",
"@kbn/config-schema": "link:../kbn-config-schema"
}
}

View file

@ -88,7 +88,7 @@ export class LegacyLoggingServer {
// We set `ops.interval` to max allowed number and `ops` filter to value
// that doesn't exist to avoid logging of ops at all, if turned on it will be
// logged by the "legacy" Kibana.
const { value: loggingConfig } = legacyLoggingConfigSchema.validate({
const loggingConfig = legacyLoggingConfigSchema.validate({
...legacyLoggingConfig,
events: {
...legacyLoggingConfig.events,

View file

@ -6,11 +6,8 @@
* Side Public License, v 1.
*/
import Joi from 'joi';
import { schema } from '@kbn/config-schema';
const HANDLED_IN_KIBANA_PLATFORM = Joi.any().description(
'This key is handled in the new platform ONLY'
);
/**
* @deprecated
*
@ -36,46 +33,65 @@ export interface LegacyLoggingConfig {
};
}
export const legacyLoggingConfigSchema = Joi.object()
.keys({
appenders: HANDLED_IN_KIBANA_PLATFORM,
loggers: HANDLED_IN_KIBANA_PLATFORM,
root: HANDLED_IN_KIBANA_PLATFORM,
silent: Joi.boolean().default(false),
quiet: Joi.boolean().when('silent', {
is: true,
then: Joi.boolean().default(true).valid(true),
otherwise: Joi.boolean().default(false),
export const legacyLoggingConfigSchema = schema.object({
silent: schema.boolean({ defaultValue: false }),
quiet: schema.conditional(
schema.siblingRef('silent'),
true,
schema.boolean({
defaultValue: true,
validate: (quiet) => {
if (!quiet) {
return 'must be true when `silent` is true';
}
},
}),
verbose: Joi.boolean().when('quiet', {
is: true,
then: Joi.valid(false).default(false),
otherwise: Joi.boolean().default(false),
schema.boolean({ defaultValue: false })
),
verbose: schema.conditional(
schema.siblingRef('quiet'),
true,
schema.boolean({
defaultValue: false,
validate: (verbose) => {
if (verbose) {
return 'must be false when `quiet` is true';
}
},
}),
events: Joi.any().default({}),
dest: Joi.string().default('stdout'),
filter: Joi.any().default({}),
json: Joi.boolean().when('dest', {
is: 'stdout',
then: Joi.boolean().default(!process.stdout.isTTY),
otherwise: Joi.boolean().default(true),
schema.boolean({ defaultValue: false })
),
events: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
dest: schema.string({ defaultValue: 'stdout' }),
filter: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
json: schema.conditional(
schema.siblingRef('dest'),
'stdout',
schema.boolean({
defaultValue: !process.stdout.isTTY,
}),
timezone: Joi.string(),
rotate: Joi.object()
.keys({
enabled: Joi.boolean().default(false),
everyBytes: Joi.number()
// > 1MB
.greater(1048576)
// < 1GB
.less(1073741825)
// 10MB
.default(10485760),
keepFiles: Joi.number().greater(2).less(1024).default(7),
pollingInterval: Joi.number().greater(5000).less(3600000).default(10000),
usePolling: Joi.boolean().default(false),
})
.default(),
})
.default();
schema.boolean({
defaultValue: true,
})
),
timezone: schema.maybe(schema.string()),
rotate: schema.object({
enabled: schema.boolean({ defaultValue: false }),
everyBytes: schema.number({
min: 1048576, // > 1MB
max: 1073741825, // < 1GB
defaultValue: 10485760, // 10MB
}),
keepFiles: schema.number({
min: 2,
max: 1024,
defaultValue: 7,
}),
pollingInterval: schema.number({
min: 5000,
max: 3600000,
defaultValue: 10000,
}),
usePolling: schema.boolean({ defaultValue: false }),
}),
});

View file

@ -14,3 +14,7 @@ export const kibanaPackageJson = {
__dirname: dirname(resolve(REPO_ROOT, 'package.json')),
...require(resolve(REPO_ROOT, 'package.json')),
};
export const isKibanaDistributable = () => {
return kibanaPackageJson.build && kibanaPackageJson.build.distributable === true;
};

View file

@ -7,7 +7,7 @@
*/
import _ from 'lodash';
import { pkg } from '../core/server/utils';
import { kibanaPackageJson as pkg } from '@kbn/utils';
import Command from './command';
import serveCommand from './serve/serve';

View file

@ -12,8 +12,7 @@ import { statSync } from 'fs';
import { resolve } from 'path';
import url from 'url';
import { getConfigPath, fromRoot } from '@kbn/utils';
import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils';
import { getConfigPath, fromRoot, isKibanaDistributable } from '@kbn/utils';
import { readKeystore } from '../keystore/read_keystore';
function canRequire(path) {
@ -65,9 +64,10 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
delete rawConfig.xpack;
}
if (opts.dev) {
set('env', 'development');
// only used to set cliArgs.envName, we don't want to inject that into the config
delete extraCliOptions.env;
if (opts.dev) {
if (!has('elasticsearch.username')) {
set('elasticsearch.username', 'kibana_system');
}
@ -184,7 +184,7 @@ export default function (program) {
.option('--plugins <path>', 'an alias for --plugin-dir', pluginDirCollector)
.option('--optimize', 'Deprecated, running the optimizer is no longer required');
if (!IS_KIBANA_DISTRIBUTABLE) {
if (!isKibanaDistributable()) {
command
.option('--oss', 'Start Kibana without X-Pack')
.option(

View file

@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
import { pkg } from '../core/server/utils';
import { kibanaPackageJson as pkg } from '@kbn/utils';
import Command from '../cli/command';
import { EncryptionConfig } from './encryption_config';

View file

@ -7,8 +7,8 @@
*/
import _ from 'lodash';
import { kibanaPackageJson as pkg } from '@kbn/utils';
import { pkg } from '../core/server/utils';
import Command from '../cli/command';
import { Keystore } from '../cli/keystore';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { pkg } from '../core/server/utils';
import { kibanaPackageJson as pkg } from '@kbn/utils';
import Command from '../cli/command';
import { listCommand } from './list';
import { installCommand } from './install';

View file

@ -6,8 +6,7 @@
* Side Public License, v 1.
*/
import { getConfigPath } from '@kbn/utils';
import { pkg } from '../../core/server/utils';
import { getConfigPath, kibanaPackageJson as pkg } from '@kbn/utils';
import { install } from './install';
import { Logger } from '../lib/logger';
import { parse, parseMilliseconds } from './settings';

View file

@ -9,7 +9,7 @@
import path from 'path';
import { statSync } from 'fs';
import { versionSatisfies, cleanVersion } from '../../legacy/utils/version';
import { versionSatisfies, cleanVersion } from './utils/version';
export function existingInstall(settings, logger) {
try {

View file

@ -7,10 +7,8 @@
*/
import { resolve } from 'path';
import expiry from 'expiry-js';
import { fromRoot } from '../../core/server/utils';
import { fromRoot } from '@kbn/utils';
function generateUrls({ version, plugin }) {
return [

View file

@ -7,8 +7,8 @@
*/
import { createAbsolutePathSerializer } from '@kbn/dev-utils';
import { fromRoot } from '@kbn/utils';
import { fromRoot } from '../../core/server/utils';
import { parseMilliseconds, parse } from './settings';
const SECOND = 1000;

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { fromRoot } from '../../core/server/utils';
import { fromRoot } from '@kbn/utils';
import { list } from './list';
import { Logger } from '../lib/logger';
import { logWarnings } from '../lib/log_warnings';

View file

@ -7,8 +7,7 @@
*/
import { resolve } from 'path';
import { fromRoot } from '../../core/server/utils';
import { fromRoot } from '@kbn/utils';
export function parse(command, options) {
const settings = {

View file

@ -0,0 +1,47 @@
/*
* 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 { configServiceMock } from './mocks';
import { ensureValidConfiguration } from './ensure_valid_configuration';
import { CriticalError } from '../errors';
describe('ensureValidConfiguration', () => {
let configService: ReturnType<typeof configServiceMock.create>;
beforeEach(() => {
jest.clearAllMocks();
configService = configServiceMock.create();
configService.getUsedPaths.mockReturnValue(Promise.resolve(['core', 'elastic']));
});
it('returns normally when there is no unused keys', async () => {
configService.getUnusedPaths.mockResolvedValue([]);
await expect(ensureValidConfiguration(configService as any)).resolves.toBeUndefined();
});
it('throws when there are some unused keys', async () => {
configService.getUnusedPaths.mockResolvedValue(['some.key', 'some.other.key']);
await expect(ensureValidConfiguration(configService as any)).rejects.toMatchInlineSnapshot(
`[Error: Unknown configuration key(s): "some.key", "some.other.key". Check for spelling errors and ensure that expected plugins are installed.]`
);
});
it('throws a `CriticalError` with the correct processExitCode value', async () => {
expect.assertions(2);
configService.getUnusedPaths.mockResolvedValue(['some.key', 'some.other.key']);
try {
await ensureValidConfiguration(configService as any);
} catch (e) {
expect(e).toBeInstanceOf(CriticalError);
expect(e.processExitCode).toEqual(64);
}
});
});

View file

@ -6,20 +6,13 @@
* Side Public License, v 1.
*/
import { getUnusedConfigKeys } from './get_unused_config_keys';
import { ConfigService } from '../../config';
import { CriticalError } from '../../errors';
import { LegacyServiceSetupConfig } from '../types';
import { ConfigService } from '@kbn/config';
import { CriticalError } from '../errors';
export async function ensureValidConfiguration(
configService: ConfigService,
{ legacyConfig, settings }: LegacyServiceSetupConfig
) {
const unusedConfigKeys = await getUnusedConfigKeys({
coreHandledConfigPaths: await configService.getUsedPaths(),
settings,
legacyConfig,
});
export async function ensureValidConfiguration(configService: ConfigService) {
await configService.validate();
const unusedConfigKeys = await configService.getUnusedPaths();
if (unusedConfigKeys.length > 0) {
const message = `Unknown configuration key(s): ${unusedConfigKeys

View file

@ -7,6 +7,7 @@
*/
export { coreDeprecationProvider } from './deprecation';
export { ensureValidConfiguration } from './ensure_valid_configuration';
export {
ConfigService,

View file

@ -8,10 +8,10 @@
import { join } from 'path';
import { PackageInfo } from '@kbn/config';
import { fromRoot } from '@kbn/utils';
import { distDir as uiSharedDepsDistDir } from '@kbn/ui-shared-deps';
import { IRouter } from '../../http';
import { UiPlugins } from '../../plugins';
import { fromRoot } from '../../utils';
import { FileHashCache } from './file_hash_cache';
import { registerRouteForBundle } from './bundles_route';

View file

@ -7,9 +7,11 @@
*/
import Path from 'path';
import { stringify } from 'querystring';
import { Env } from '@kbn/config';
import { schema } from '@kbn/config-schema';
import { fromRoot } from '@kbn/utils';
import { fromRoot } from '../utils';
import { InternalCoreSetup } from '../internal_types';
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
@ -49,6 +51,41 @@ export class CoreApp {
});
});
// remove trailing slash catch-all
router.get(
{
path: '/{path*}',
validate: {
params: schema.object({
path: schema.maybe(schema.string()),
}),
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
},
},
async (context, req, res) => {
const { query, params } = req;
const { path } = params;
if (!path || !path.endsWith('/')) {
return res.notFound();
}
const basePath = httpSetup.basePath.get(req);
let rewrittenPath = path.slice(0, -1);
if (`/${path}`.startsWith(basePath)) {
rewrittenPath = rewrittenPath.substring(basePath.length);
}
const querystring = query ? stringify(query) : undefined;
const url = `${basePath}/${rewrittenPath}${querystring ? `?${querystring}` : ''}`;
return res.redirected({
headers: {
location: url,
},
});
}
);
router.get({ path: '/core', validate: false }, async (context, req, res) =>
res.ok({ body: { version: '0.0.1' } })
);

View file

@ -0,0 +1,58 @@
/*
* 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 * as kbnTestServer from '../../../test_helpers/kbn_server';
import { Root } from '../../root';
describe('Core app routes', () => {
let root: Root;
beforeAll(async function () {
root = kbnTestServer.createRoot({
plugins: { initialize: false },
server: {
basePath: '/base-path',
},
});
await root.setup();
await root.start();
});
afterAll(async function () {
await root.shutdown();
});
describe('`/{path*}` route', () => {
it('redirects requests to include the basePath', async () => {
const response = await kbnTestServer.request.get(root, '/some-path/').expect(302);
expect(response.get('location')).toEqual('/base-path/some-path');
});
it('includes the query in the redirect', async () => {
const response = await kbnTestServer.request.get(root, '/some-path/?foo=bar').expect(302);
expect(response.get('location')).toEqual('/base-path/some-path?foo=bar');
});
it('does not redirect if the path does not end with `/`', async () => {
await kbnTestServer.request.get(root, '/some-path').expect(404);
});
it('does not add the basePath if the path already contains it', async () => {
const response = await kbnTestServer.request.get(root, '/base-path/foo/').expect(302);
expect(response.get('location')).toEqual('/base-path/foo');
});
});
describe('`/` route', () => {
it('prevails on the `/{path*}` route', async () => {
const response = await kbnTestServer.request.get(root, '/').expect(302);
expect(response.get('location')).toEqual('/base-path/app/home');
});
});
});

View file

@ -11,6 +11,7 @@ import { IHttpConfig, SslConfig, sslSchema } from '@kbn/server-http-tools';
import { hostname } from 'os';
import url from 'url';
import { ServiceConfigDescriptor } from '../internal_types';
import { CspConfigType, CspConfig, ICspConfig } from '../csp';
import { ExternalUrlConfig, IExternalUrlConfig } from '../external_url';
@ -20,136 +21,138 @@ const hostURISchema = schema.uri({ scheme: ['http', 'https'] });
const match = (regex: RegExp, errorMsg: string) => (str: string) =>
regex.test(str) ? undefined : errorMsg;
// before update to make sure it's in sync with validation rules in Legacy
// https://github.com/elastic/kibana/blob/master/src/legacy/server/config/schema.js
export const config = {
path: 'server' as const,
schema: schema.object(
{
name: schema.string({ defaultValue: () => hostname() }),
autoListen: schema.boolean({ defaultValue: true }),
publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })),
basePath: schema.maybe(
schema.string({
validate: match(validBasePathRegex, "must start with a slash, don't end with one"),
})
),
cors: schema.object(
{
enabled: schema.boolean({ defaultValue: false }),
allowCredentials: schema.boolean({ defaultValue: false }),
allowOrigin: schema.oneOf(
[
schema.arrayOf(hostURISchema, { minSize: 1 }),
schema.arrayOf(schema.literal('*'), { minSize: 1, maxSize: 1 }),
],
{
defaultValue: ['*'],
}
),
},
{
validate(value) {
if (value.allowCredentials === true && value.allowOrigin.includes('*')) {
return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.';
}
},
}
),
customResponseHeaders: schema.recordOf(schema.string(), schema.any(), {
defaultValue: {},
}),
host: schema.string({
defaultValue: 'localhost',
hostname: true,
}),
maxPayload: schema.byteSize({
defaultValue: '1048576b',
}),
port: schema.number({
defaultValue: 5601,
}),
rewriteBasePath: schema.boolean({ defaultValue: false }),
ssl: sslSchema,
keepaliveTimeout: schema.number({
defaultValue: 120000,
}),
socketTimeout: schema.number({
defaultValue: 120000,
}),
compression: schema.object({
enabled: schema.boolean({ defaultValue: true }),
referrerWhitelist: schema.maybe(
schema.arrayOf(
schema.string({
hostname: true,
}),
{ minSize: 1 }
)
),
}),
uuid: schema.maybe(
schema.string({
validate: match(uuidRegexp, 'must be a valid uuid'),
})
),
xsrf: schema.object({
disableProtection: schema.boolean({ defaultValue: false }),
allowlist: schema.arrayOf(
schema.string({ validate: match(/^\//, 'must start with a slash') }),
{ defaultValue: [] }
),
}),
requestId: schema.object(
{
allowFromAnyIp: schema.boolean({ defaultValue: false }),
ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }),
},
{
validate(value) {
if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) {
return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`;
}
},
}
),
},
{
validate: (rawConfig) => {
if (!rawConfig.basePath && rawConfig.rewriteBasePath) {
return 'cannot use [rewriteBasePath] when [basePath] is not specified';
}
if (rawConfig.publicBaseUrl) {
const parsedUrl = url.parse(rawConfig.publicBaseUrl);
if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) {
return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`;
const configSchema = schema.object(
{
name: schema.string({ defaultValue: () => hostname() }),
autoListen: schema.boolean({ defaultValue: true }),
publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })),
basePath: schema.maybe(
schema.string({
validate: match(validBasePathRegex, "must start with a slash, don't end with one"),
})
),
cors: schema.object(
{
enabled: schema.boolean({ defaultValue: false }),
allowCredentials: schema.boolean({ defaultValue: false }),
allowOrigin: schema.oneOf(
[
schema.arrayOf(hostURISchema, { minSize: 1 }),
schema.arrayOf(schema.literal('*'), { minSize: 1, maxSize: 1 }),
],
{
defaultValue: ['*'],
}
if (parsedUrl.path !== (rawConfig.basePath ?? '/')) {
return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`;
}
}
if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) {
return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false';
}
if (
rawConfig.ssl.enabled &&
rawConfig.ssl.redirectHttpFromPort !== undefined &&
rawConfig.ssl.redirectHttpFromPort === rawConfig.port
) {
return (
'Kibana does not accept http traffic to [port] when ssl is ' +
'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' +
`cannot be configured to the same value. Both are [${rawConfig.port}].`
);
}
),
},
}
),
{
validate(value) {
if (value.allowCredentials === true && value.allowOrigin.includes('*')) {
return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.';
}
},
}
),
customResponseHeaders: schema.recordOf(schema.string(), schema.any(), {
defaultValue: {},
}),
host: schema.string({
defaultValue: 'localhost',
hostname: true,
}),
maxPayload: schema.byteSize({
defaultValue: '1048576b',
}),
port: schema.number({
defaultValue: 5601,
}),
rewriteBasePath: schema.boolean({ defaultValue: false }),
ssl: sslSchema,
keepaliveTimeout: schema.number({
defaultValue: 120000,
}),
socketTimeout: schema.number({
defaultValue: 120000,
}),
compression: schema.object({
enabled: schema.boolean({ defaultValue: true }),
referrerWhitelist: schema.maybe(
schema.arrayOf(
schema.string({
hostname: true,
}),
{ minSize: 1 }
)
),
}),
uuid: schema.maybe(
schema.string({
validate: match(uuidRegexp, 'must be a valid uuid'),
})
),
xsrf: schema.object({
disableProtection: schema.boolean({ defaultValue: false }),
allowlist: schema.arrayOf(
schema.string({ validate: match(/^\//, 'must start with a slash') }),
{ defaultValue: [] }
),
}),
requestId: schema.object(
{
allowFromAnyIp: schema.boolean({ defaultValue: false }),
ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }),
},
{
validate(value) {
if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) {
return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`;
}
},
}
),
},
{
validate: (rawConfig) => {
if (!rawConfig.basePath && rawConfig.rewriteBasePath) {
return 'cannot use [rewriteBasePath] when [basePath] is not specified';
}
if (rawConfig.publicBaseUrl) {
const parsedUrl = url.parse(rawConfig.publicBaseUrl);
if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) {
return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`;
}
if (parsedUrl.path !== (rawConfig.basePath ?? '/')) {
return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`;
}
}
if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) {
return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false';
}
if (
rawConfig.ssl.enabled &&
rawConfig.ssl.redirectHttpFromPort !== undefined &&
rawConfig.ssl.redirectHttpFromPort === rawConfig.port
) {
return (
'Kibana does not accept http traffic to [port] when ssl is ' +
'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' +
`cannot be configured to the same value. Both are [${rawConfig.port}].`
);
}
},
}
);
export type HttpConfigType = TypeOf<typeof configSchema>;
export const config: ServiceConfigDescriptor<HttpConfigType> = {
path: 'server' as const,
schema: configSchema,
deprecations: ({ rename }) => [rename('maxPayloadBytes', 'maxPayload')],
};
export type HttpConfigType = TypeOf<typeof config.schema>;
export class HttpConfig implements IHttpConfig {
public name: string;

View file

@ -12,8 +12,6 @@ import {
legacyClusterClientInstanceMock,
} from './core_service.test.mocks';
import Boom from '@hapi/boom';
import { Request } from '@hapi/hapi';
import { errors as esErrors } from 'elasticsearch';
import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy';
@ -22,16 +20,6 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import * as kbnTestServer from '../../../test_helpers/kbn_server';
import { InternalElasticsearchServiceStart } from '../../elasticsearch';
interface User {
id: string;
roles?: string[];
}
interface StorageData {
value: User;
expires: number;
}
const cookieOptions = {
name: 'sid',
encryptionKey: 'something_at_least_32_characters',
@ -197,172 +185,6 @@ describe('http service', () => {
});
});
describe('legacy server', () => {
describe('#registerAuth()', () => {
const sessionDurationMs = 1000;
let root: ReturnType<typeof kbnTestServer.createRoot>;
beforeEach(async () => {
root = kbnTestServer.createRoot({ plugins: { initialize: false } });
}, 30000);
afterEach(async () => {
MockLegacyScopedClusterClient.mockClear();
await root.shutdown();
});
it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => {
const { http } = await root.setup();
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
cookieOptions
);
http.registerAuth((req, res, toolkit) => {
if (req.headers.authorization) {
const user = { id: '42' };
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
return toolkit.authenticated({ state: user });
} else {
return res.unauthorized();
}
});
await root.start();
const legacyUrl = '/legacy';
const kbnServer = kbnTestServer.getKbnServer(root);
kbnServer.server.route({
method: 'GET',
path: legacyUrl,
handler: () => 'ok from legacy server',
});
const response = await kbnTestServer.request
.get(root, legacyUrl)
.expect(200, 'ok from legacy server');
expect(response.header['set-cookie']).toHaveLength(1);
});
it('passes authHeaders as request headers to the legacy platform', async () => {
const token = 'Basic: name:password';
const { http } = await root.setup();
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
cookieOptions
);
http.registerAuth((req, res, toolkit) => {
if (req.headers.authorization) {
const user = { id: '42' };
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
return toolkit.authenticated({
state: user,
requestHeaders: {
authorization: token,
},
});
} else {
return res.unauthorized();
}
});
await root.start();
const legacyUrl = '/legacy';
const kbnServer = kbnTestServer.getKbnServer(root);
kbnServer.server.route({
method: 'GET',
path: legacyUrl,
handler: (req: Request) => ({
authorization: req.headers.authorization,
custom: req.headers.custom,
}),
});
await kbnTestServer.request
.get(root, legacyUrl)
.set({ custom: 'custom-header' })
.expect(200, { authorization: token, custom: 'custom-header' });
});
it('attach security header to a successful response handled by Legacy platform', async () => {
const authResponseHeader = {
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
};
const { http } = await root.setup();
const { registerAuth } = http;
registerAuth((req, res, toolkit) => {
return toolkit.authenticated({ responseHeaders: authResponseHeader });
});
await root.start();
const kbnServer = kbnTestServer.getKbnServer(root);
kbnServer.server.route({
method: 'GET',
path: '/legacy',
handler: () => 'ok',
});
const response = await kbnTestServer.request.get(root, '/legacy').expect(200);
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
});
it('attach security header to an error response handled by Legacy platform', async () => {
const authResponseHeader = {
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
};
const { http } = await root.setup();
const { registerAuth } = http;
registerAuth((req, res, toolkit) => {
return toolkit.authenticated({ responseHeaders: authResponseHeader });
});
await root.start();
const kbnServer = kbnTestServer.getKbnServer(root);
kbnServer.server.route({
method: 'GET',
path: '/legacy',
handler: () => {
throw Boom.badRequest();
},
});
const response = await kbnTestServer.request.get(root, '/legacy').expect(400);
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
});
});
describe('#basePath()', () => {
let root: ReturnType<typeof kbnTestServer.createRoot>;
beforeEach(async () => {
root = kbnTestServer.createRoot({ plugins: { initialize: false } });
}, 30000);
afterEach(async () => await root.shutdown());
it('basePath information for an incoming request is available in legacy server', async () => {
const reqBasePath = '/requests-specific-base-path';
const { http } = await root.setup();
http.registerOnPreRouting((req, res, toolkit) => {
http.basePath.set(req, reqBasePath);
return toolkit.next();
});
await root.start();
const legacyUrl = '/legacy';
const kbnServer = kbnTestServer.getKbnServer(root);
kbnServer.server.route({
method: 'GET',
path: legacyUrl,
handler: kbnServer.newPlatform.setup.core.http.basePath.get,
});
await kbnTestServer.request.get(root, legacyUrl).expect(200, reqBasePath);
});
});
});
describe('legacy elasticsearch client', () => {
let root: ReturnType<typeof kbnTestServer.createRoot>;
beforeEach(async () => {

View file

@ -14,7 +14,7 @@ const mockGetTranslationPaths = getTranslationPaths as jest.Mock;
jest.mock('./get_translation_paths', () => ({
getTranslationPaths: jest.fn().mockResolvedValue([]),
}));
jest.mock('../utils', () => ({
jest.mock('@kbn/utils', () => ({
fromRoot: jest.fn().mockImplementation((path: string) => path),
}));

View file

@ -7,7 +7,7 @@
*/
import { basename } from 'path';
import { fromRoot } from '../utils';
import { fromRoot } from '@kbn/utils';
import { getTranslationPaths } from './get_translation_paths';
export const getKibanaTranslationFiles = async (

View file

@ -406,8 +406,6 @@ export type {
SavedObjectsMigrationVersion,
} from './types';
export type { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig } from './legacy';
export { ServiceStatusLevels } from './status';
export type { CoreStatus, ServiceStatus, ServiceStatusLevel, StatusServiceSetup } from './status';

View file

@ -1,27 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`once LegacyService is set up with connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = `
Array [
Array [
Object {
"logging": Object {
"verbose": true,
},
"path": Object {},
},
],
]
`;
exports[`once LegacyService is set up without connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = `
Array [
Array [
Object {
"logging": Object {
"verbose": true,
},
"path": Object {},
},
],
]
`;

View file

@ -1,59 +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 { ensureValidConfiguration } from './ensure_valid_configuration';
import { getUnusedConfigKeys } from './get_unused_config_keys';
import { configServiceMock } from '../../config/mocks';
jest.mock('./get_unused_config_keys');
describe('ensureValidConfiguration', () => {
let configService: ReturnType<typeof configServiceMock.create>;
beforeEach(() => {
jest.clearAllMocks();
configService = configServiceMock.create();
configService.getUsedPaths.mockReturnValue(Promise.resolve(['core', 'elastic']));
(getUnusedConfigKeys as any).mockImplementation(() => []);
});
it('calls getUnusedConfigKeys with correct parameters', async () => {
await ensureValidConfiguration(
configService as any,
{
settings: 'settings',
legacyConfig: 'pluginExtendedConfig',
} as any
);
expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1);
expect(getUnusedConfigKeys).toHaveBeenCalledWith({
coreHandledConfigPaths: ['core', 'elastic'],
settings: 'settings',
legacyConfig: 'pluginExtendedConfig',
});
});
it('returns normally when there is no unused keys', async () => {
await expect(
ensureValidConfiguration(configService as any, {} as any)
).resolves.toBeUndefined();
expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1);
});
it('throws when there are some unused keys', async () => {
(getUnusedConfigKeys as any).mockImplementation(() => ['some.key', 'some.other.key']);
await expect(
ensureValidConfiguration(configService as any, {} as any)
).rejects.toMatchInlineSnapshot(
`[Error: Unknown configuration key(s): "some.key", "some.other.key". Check for spelling errors and ensure that expected plugins are installed.]`
);
});
});

View file

@ -1,163 +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 { LegacyConfig, LegacyVars } from '../types';
import { getUnusedConfigKeys } from './get_unused_config_keys';
describe('getUnusedConfigKeys', () => {
beforeEach(() => {
jest.resetAllMocks();
});
const getConfig = (values: LegacyVars = {}): LegacyConfig =>
({
get: () => values as any,
} as LegacyConfig);
describe('not using core or plugin specs', () => {
it('should return an empty list for empty parameters', async () => {
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: [],
settings: {},
legacyConfig: getConfig(),
})
).toEqual([]);
});
it('returns empty list when config and settings have the same properties', async () => {
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: [],
settings: {
presentInBoth: true,
alsoInBoth: 'someValue',
},
legacyConfig: getConfig({
presentInBoth: true,
alsoInBoth: 'someValue',
}),
})
).toEqual([]);
});
it('returns empty list when config has entries not present in settings', async () => {
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: [],
settings: {
presentInBoth: true,
},
legacyConfig: getConfig({
presentInBoth: true,
onlyInConfig: 'someValue',
}),
})
).toEqual([]);
});
it('returns the list of properties from settings not present in config', async () => {
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: [],
settings: {
presentInBoth: true,
onlyInSetting: 'value',
},
legacyConfig: getConfig({
presentInBoth: true,
}),
})
).toEqual(['onlyInSetting']);
});
it('correctly handle nested properties', async () => {
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: [],
settings: {
elasticsearch: {
username: 'foo',
password: 'bar',
},
},
legacyConfig: getConfig({
elasticsearch: {
username: 'foo',
onlyInConfig: 'default',
},
}),
})
).toEqual(['elasticsearch.password']);
});
it('correctly handle "env" specific case', async () => {
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: [],
settings: {
env: 'development',
},
legacyConfig: getConfig({
env: {
name: 'development',
},
}),
})
).toEqual([]);
});
it('correctly handle array properties', async () => {
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: [],
settings: {
prop: ['a', 'b', 'c'],
},
legacyConfig: getConfig({
prop: ['a'],
}),
})
).toEqual([]);
});
});
it('ignores properties managed by the new platform', async () => {
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: ['core', 'foo.bar'],
settings: {
core: {
prop: 'value',
},
foo: {
bar: true,
dolly: true,
},
},
legacyConfig: getConfig({}),
})
).toEqual(['foo.dolly']);
});
it('handles array values', async () => {
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: ['core', 'array'],
settings: {
core: {
prop: 'value',
array: [1, 2, 3],
},
array: ['some', 'values'],
},
legacyConfig: getConfig({}),
})
).toEqual([]);
});
});

View file

@ -1,42 +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 { difference } from 'lodash';
import { getFlattenedObject } from '@kbn/std';
import { hasConfigPathIntersection } from '../../config';
import { LegacyConfig, LegacyVars } from '../types';
const getFlattenedKeys = (object: object) => Object.keys(getFlattenedObject(object));
export async function getUnusedConfigKeys({
coreHandledConfigPaths,
settings,
legacyConfig,
}: {
coreHandledConfigPaths: string[];
settings: LegacyVars;
legacyConfig: LegacyConfig;
}) {
const inputKeys = getFlattenedKeys(settings);
const appliedKeys = getFlattenedKeys(legacyConfig.get());
if (inputKeys.includes('env')) {
// env is a special case key, see https://github.com/elastic/kibana/blob/848bf17b/src/legacy/server/config/config.js#L74
// where it is deleted from the settings before being injected into the schema via context and
// then renamed to `env.name` https://github.com/elastic/kibana/blob/848bf17/src/legacy/server/config/schema.js#L17
inputKeys[inputKeys.indexOf('env')] = 'env.name';
}
// Filter out keys that are marked as used in the core (e.g. by new core plugins).
return difference(inputKeys, appliedKeys).filter(
(unusedConfigKey) =>
!coreHandledConfigPaths.some((usedInCoreConfigKey) =>
hasConfigPathIntersection(unusedConfigKey, usedInCoreConfigKey)
)
);
}

View file

@ -1,9 +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.
*/
export { ensureValidConfiguration } from './ensure_valid_configuration';

View file

@ -6,16 +6,6 @@
* Side Public License, v 1.
*/
/** @internal */
export { ensureValidConfiguration } from './config';
/** @internal */
export type { ILegacyService } from './legacy_service';
export { LegacyService } from './legacy_service';
/** @internal */
export type {
LegacyVars,
LegacyConfig,
LegacyServiceSetupDeps,
LegacyServiceStartDeps,
LegacyServiceSetupConfig,
} from './types';

View file

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

View file

@ -8,26 +8,14 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import { LegacyService } from './legacy_service';
import { LegacyConfig, LegacyServiceSetupDeps } from './types';
type LegacyServiceMock = jest.Mocked<PublicMethodsOf<LegacyService> & { legacyId: symbol }>;
type LegacyServiceMock = jest.Mocked<PublicMethodsOf<LegacyService>>;
const createLegacyServiceMock = (): LegacyServiceMock => ({
legacyId: Symbol(),
setupLegacyConfig: jest.fn(),
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
});
const createLegacyConfigMock = (): jest.Mocked<LegacyConfig> => ({
get: jest.fn(),
has: jest.fn(),
set: jest.fn(),
});
export const legacyServiceMock = {
create: createLegacyServiceMock,
createSetupContract: (deps: LegacyServiceSetupDeps) => createLegacyServiceMock().setup(deps),
createLegacyConfig: createLegacyConfigMock,
};

View file

@ -0,0 +1,18 @@
/*
* 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 const reconfigureLoggingMock = jest.fn();
export const setupLoggingMock = jest.fn();
export const setupLoggingRotateMock = jest.fn();
jest.doMock('@kbn/legacy-logging', () => ({
...(jest.requireActual('@kbn/legacy-logging') as any),
reconfigureLogging: reconfigureLoggingMock,
setupLogging: setupLoggingMock,
setupLoggingRotate: setupLoggingRotateMock,
}));

View file

@ -6,35 +6,22 @@
* Side Public License, v 1.
*/
jest.mock('../../../legacy/server/kbn_server');
import {
setupLoggingMock,
setupLoggingRotateMock,
reconfigureLoggingMock,
} from './legacy_service.test.mocks';
import { BehaviorSubject, throwError } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import moment from 'moment';
import { REPO_ROOT } from '@kbn/dev-utils';
import KbnServer from '../../../legacy/server/kbn_server';
import { Config, Env, ObjectToConfigAdapter } from '../config';
import { DiscoveredPlugin } from '../plugins';
import { getEnvOptions, configServiceMock } from '../config/mocks';
import { loggingSystemMock } from '../logging/logging_system.mock';
import { contextServiceMock } from '../context/context_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock';
import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock';
import { httpResourcesMock } from '../http_resources/http_resources_service.mock';
import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service';
import { environmentServiceMock } from '../environment/environment_service.mock';
import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types';
import { LegacyService } from './legacy_service';
import { coreMock } from '../mocks';
import { statusServiceMock } from '../status/status_service.mock';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { metricsServiceMock } from '../metrics/metrics_service.mock';
import { i18nServiceMock } from '../i18n/i18n_service.mock';
import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock';
const MockKbnServer: jest.Mock<KbnServer> = KbnServer as any;
import { LegacyService, LegacyServiceSetupDeps } from './legacy_service';
let coreId: symbol;
let env: Env;
@ -42,71 +29,16 @@ let config$: BehaviorSubject<Config>;
let setupDeps: LegacyServiceSetupDeps;
let startDeps: LegacyServiceStartDeps;
const logger = loggingSystemMock.create();
let configService: ReturnType<typeof configServiceMock.create>;
let environmentSetup: ReturnType<typeof environmentServiceMock.createSetupContract>;
beforeEach(() => {
coreId = Symbol();
env = Env.createDefault(REPO_ROOT, getEnvOptions());
configService = configServiceMock.create();
environmentSetup = environmentServiceMock.createSetupContract();
MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve());
MockKbnServer.prototype.listen = jest.fn();
setupDeps = {
core: {
capabilities: capabilitiesServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
elasticsearch: { legacy: {} } as any,
i18n: i18nServiceMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
http: {
...httpServiceMock.createInternalSetupContract(),
auth: {
getAuthHeaders: () => undefined,
} as any,
},
httpResources: httpResourcesMock.createSetupContract(),
savedObjects: savedObjectsServiceMock.createInternalSetupContract(),
plugins: {
initialized: true,
contracts: new Map([['plugin-id', 'plugin-value']]),
},
rendering: renderingServiceMock,
environment: environmentSetup,
status: statusServiceMock.createInternalSetupContract(),
logging: loggingServiceMock.createInternalSetupContract(),
metrics: metricsServiceMock.createInternalSetupContract(),
deprecations: deprecationsServiceMock.createInternalSetupContract(),
},
plugins: { 'plugin-id': 'plugin-value' },
uiPlugins: {
public: new Map([['plugin-id', {} as DiscoveredPlugin]]),
internal: new Map([
[
'plugin-id',
{
requiredBundles: [],
version: '8.0.0',
publicTargetDir: 'path/to/target/public',
publicAssetsDir: '/plugins/name/assets/',
},
],
]),
browserConfigs: new Map(),
},
};
startDeps = {
core: {
...coreMock.createInternalStart(),
plugins: { contracts: new Map() },
},
plugins: {},
http: httpServiceMock.createInternalSetupContract(),
};
config$ = new BehaviorSubject<Config>(
@ -117,78 +49,78 @@ beforeEach(() => {
);
configService.getConfig$.mockReturnValue(config$);
configService.getUsedPaths.mockResolvedValue(['foo.bar']);
});
afterEach(() => {
jest.clearAllMocks();
setupLoggingMock.mockReset();
setupLoggingRotateMock.mockReset();
reconfigureLoggingMock.mockReset();
});
describe('once LegacyService is set up with connection info', () => {
test('creates legacy kbnServer and calls `listen`.', async () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
const legacyService = new LegacyService({
coreId,
env,
logger,
configService,
describe('#setup', () => {
it('initializes legacy logging', async () => {
const opsConfig = {
interval: moment.duration(5, 'second'),
};
const opsConfig$ = new BehaviorSubject(opsConfig);
const loggingConfig = {
foo: 'bar',
};
const loggingConfig$ = new BehaviorSubject(loggingConfig);
configService.atPath.mockImplementation((path) => {
if (path === 'ops') {
return opsConfig$;
}
if (path === 'logging') {
return loggingConfig$;
}
return new BehaviorSubject({});
});
await legacyService.setupLegacyConfig();
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
expect(MockKbnServer).toHaveBeenCalledTimes(1);
expect(MockKbnServer).toHaveBeenCalledWith(
{ path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value
expect.objectContaining({ get: expect.any(Function) }),
expect.any(Object)
);
expect(MockKbnServer.mock.calls[0][1].get()).toEqual(
expect.objectContaining({
path: expect.objectContaining({ autoListen: true }),
server: expect.objectContaining({ autoListen: true }),
})
);
const [mockKbnServer] = MockKbnServer.mock.instances;
expect(mockKbnServer.listen).toHaveBeenCalledTimes(1);
expect(mockKbnServer.close).not.toHaveBeenCalled();
});
test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false }));
const legacyService = new LegacyService({
coreId,
env,
logger,
configService: configService as any,
});
await legacyService.setupLegacyConfig();
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
expect(MockKbnServer).toHaveBeenCalledTimes(1);
expect(MockKbnServer).toHaveBeenCalledWith(
{ path: { autoListen: false }, server: { autoListen: true } },
expect.objectContaining({ get: expect.any(Function) }),
expect.any(Object)
await legacyService.setup(setupDeps);
expect(setupLoggingMock).toHaveBeenCalledTimes(1);
expect(setupLoggingMock).toHaveBeenCalledWith(
setupDeps.http.server,
loggingConfig,
opsConfig.interval.asMilliseconds()
);
const legacyConfig = MockKbnServer.mock.calls[0][1].get();
expect(legacyConfig.path.autoListen).toBe(false);
expect(legacyConfig.server.autoListen).toBe(true);
const [mockKbnServer] = MockKbnServer.mock.instances;
expect(mockKbnServer.ready).toHaveBeenCalledTimes(1);
expect(mockKbnServer.listen).not.toHaveBeenCalled();
expect(mockKbnServer.close).not.toHaveBeenCalled();
expect(setupLoggingRotateMock).toHaveBeenCalledTimes(1);
expect(setupLoggingRotateMock).toHaveBeenCalledWith(setupDeps.http.server, loggingConfig);
});
test('creates legacy kbnServer and closes it if `listen` fails.', async () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed'));
it('reloads the logging config when the config changes', async () => {
const opsConfig = {
interval: moment.duration(5, 'second'),
};
const opsConfig$ = new BehaviorSubject(opsConfig);
const loggingConfig = {
foo: 'bar',
};
const loggingConfig$ = new BehaviorSubject(loggingConfig);
configService.atPath.mockImplementation((path) => {
if (path === 'ops') {
return opsConfig$;
}
if (path === 'logging') {
return loggingConfig$;
}
return new BehaviorSubject({});
});
const legacyService = new LegacyService({
coreId,
env,
@ -196,19 +128,48 @@ describe('once LegacyService is set up with connection info', () => {
configService: configService as any,
});
await legacyService.setupLegacyConfig();
await legacyService.setup(setupDeps);
await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"something failed"`
expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1);
expect(reconfigureLoggingMock).toHaveBeenCalledWith(
setupDeps.http.server,
loggingConfig,
opsConfig.interval.asMilliseconds()
);
const [mockKbnServer] = MockKbnServer.mock.instances;
expect(mockKbnServer.listen).toHaveBeenCalled();
expect(mockKbnServer.close).toHaveBeenCalled();
loggingConfig$.next({
foo: 'changed',
});
expect(reconfigureLoggingMock).toHaveBeenCalledTimes(2);
expect(reconfigureLoggingMock).toHaveBeenCalledWith(
setupDeps.http.server,
{ foo: 'changed' },
opsConfig.interval.asMilliseconds()
);
});
test('throws if fails to retrieve initial config.', async () => {
configService.getConfig$.mockReturnValue(throwError(new Error('something failed')));
it('stops reloading logging config once the service is stopped', async () => {
const opsConfig = {
interval: moment.duration(5, 'second'),
};
const opsConfig$ = new BehaviorSubject(opsConfig);
const loggingConfig = {
foo: 'bar',
};
const loggingConfig$ = new BehaviorSubject(loggingConfig);
configService.atPath.mockImplementation((path) => {
if (path === 'ops') {
return opsConfig$;
}
if (path === 'logging') {
return loggingConfig$;
}
return new BehaviorSubject({});
});
const legacyService = new LegacyService({
coreId,
env,
@ -216,150 +177,21 @@ describe('once LegacyService is set up with connection info', () => {
configService: configService as any,
});
await expect(legacyService.setupLegacyConfig()).rejects.toThrowErrorMatchingInlineSnapshot(
`"something failed"`
);
await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()"`
);
await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Legacy service is not setup yet."`
);
expect(MockKbnServer).not.toHaveBeenCalled();
});
test('reconfigures logging configuration if new config is received.', async () => {
const legacyService = new LegacyService({
coreId,
env,
logger,
configService: configService as any,
});
await legacyService.setupLegacyConfig();
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
const [mockKbnServer] = MockKbnServer.mock.instances as Array<jest.Mocked<KbnServer>>;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } }));
expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot(
`applyLoggingConfiguration params`
expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1);
expect(reconfigureLoggingMock).toHaveBeenCalledWith(
setupDeps.http.server,
loggingConfig,
opsConfig.interval.asMilliseconds()
);
});
test('logs error if re-configuring fails.', async () => {
const legacyService = new LegacyService({
coreId,
env,
logger,
configService: configService as any,
});
await legacyService.setupLegacyConfig();
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
await legacyService.stop();
const [mockKbnServer] = MockKbnServer.mock.instances as Array<jest.Mocked<KbnServer>>;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
expect(loggingSystemMock.collect(logger).error).toEqual([]);
const configError = new Error('something went wrong');
mockKbnServer.applyLoggingConfiguration.mockImplementation(() => {
throw configError;
loggingConfig$.next({
foo: 'changed',
});
config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } }));
expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]);
});
test('logs error if config service fails.', async () => {
const legacyService = new LegacyService({
coreId,
env,
logger,
configService: configService as any,
});
await legacyService.setupLegacyConfig();
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
const [mockKbnServer] = MockKbnServer.mock.instances;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
expect(loggingSystemMock.collect(logger).error).toEqual([]);
const configError = new Error('something went wrong');
config$.error(configError);
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]);
expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1);
});
});
describe('once LegacyService is set up without connection info', () => {
let legacyService: LegacyService;
beforeEach(async () => {
legacyService = new LegacyService({ coreId, env, logger, configService: configService as any });
await legacyService.setupLegacyConfig();
await legacyService.setup(setupDeps);
await legacyService.start(startDeps);
});
test('creates legacy kbnServer with `autoListen: false`.', () => {
expect(MockKbnServer).toHaveBeenCalledTimes(1);
expect(MockKbnServer).toHaveBeenCalledWith(
{ path: {}, server: { autoListen: true } },
expect.objectContaining({ get: expect.any(Function) }),
expect.any(Object)
);
expect(MockKbnServer.mock.calls[0][1].get()).toEqual(
expect.objectContaining({
server: expect.objectContaining({ autoListen: true }),
})
);
});
test('reconfigures logging configuration if new config is received.', async () => {
const [mockKbnServer] = MockKbnServer.mock.instances as Array<jest.Mocked<KbnServer>>;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } }));
expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot(
`applyLoggingConfiguration params`
);
});
});
describe('start', () => {
test('Cannot start without setup phase', async () => {
const legacyService = new LegacyService({
coreId,
env,
logger,
configService: configService as any,
});
await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Legacy service is not setup yet."`
);
});
});
test('Sets the server.uuid property on the legacy configuration', async () => {
configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
const legacyService = new LegacyService({
coreId,
env,
logger,
configService: configService as any,
});
environmentSetup.instanceUuid = 'UUID_FROM_SERVICE';
const { legacyConfig } = await legacyService.setupLegacyConfig();
await legacyService.setup(setupDeps);
expect(legacyConfig.get('server.uuid')).toBe('UUID_FROM_SERVICE');
});

View file

@ -6,141 +6,61 @@
* Side Public License, v 1.
*/
import { combineLatest, ConnectableObservable, Observable, Subscription } from 'rxjs';
import { first, map, publishReplay, tap } from 'rxjs/operators';
import { combineLatest, Observable, Subscription } from 'rxjs';
import { first } from 'rxjs/operators';
import { Server } from '@hapi/hapi';
import type { PublicMethodsOf } from '@kbn/utility-types';
import { PathConfigType } from '@kbn/utils';
import type { RequestHandlerContext } from 'src/core/server';
// @ts-expect-error legacy config class
import { Config as LegacyConfigClass } from '../../../legacy/server/config';
import { CoreService } from '../../types';
import { Config } from '../config';
import { CoreContext } from '../core_context';
import { CspConfigType, config as cspConfig } from '../csp';
import {
HttpConfig,
HttpConfigType,
config as httpConfig,
IRouter,
RequestHandlerContextProvider,
} from '../http';
reconfigureLogging,
setupLogging,
setupLoggingRotate,
LegacyLoggingConfig,
} from '@kbn/legacy-logging';
import { CoreContext } from '../core_context';
import { config as loggingConfig } from '../logging';
import { opsConfig, OpsConfigType } from '../metrics';
import { Logger } from '../logging';
import { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig, LegacyVars } from './types';
import { ExternalUrlConfigType, config as externalUrlConfig } from '../external_url';
import { CoreSetup, CoreStart } from '..';
import { InternalHttpServiceSetup } from '../http';
interface LegacyKbnServer {
applyLoggingConfiguration: (settings: Readonly<LegacyVars>) => void;
listen: () => Promise<void>;
ready: () => Promise<void>;
close: () => Promise<void>;
}
function getLegacyRawConfig(config: Config, pathConfig: PathConfigType) {
const rawConfig = config.toRaw();
// Elasticsearch config is solely handled by the core and legacy platform
// shouldn't have direct access to it.
if (rawConfig.elasticsearch !== undefined) {
delete rawConfig.elasticsearch;
}
return {
...rawConfig,
// We rely heavily in the default value of 'path.data' in the legacy world and,
// since it has been moved to NP, it won't show up in RawConfig.
path: pathConfig,
};
export interface LegacyServiceSetupDeps {
http: InternalHttpServiceSetup;
}
/** @internal */
export type ILegacyService = PublicMethodsOf<LegacyService>;
/** @internal */
export class LegacyService implements CoreService {
/** Symbol to represent the legacy platform as a fake "plugin". Used by the ContextService */
public readonly legacyId = Symbol();
export class LegacyService {
private readonly log: Logger;
private readonly httpConfig$: Observable<HttpConfig>;
private kbnServer?: LegacyKbnServer;
private readonly opsConfig$: Observable<OpsConfigType>;
private readonly legacyLoggingConfig$: Observable<LegacyLoggingConfig>;
private configSubscription?: Subscription;
private setupDeps?: LegacyServiceSetupDeps;
private update$?: ConnectableObservable<[Config, PathConfigType]>;
private legacyRawConfig?: LegacyConfig;
private settings?: LegacyVars;
constructor(private readonly coreContext: CoreContext) {
constructor(coreContext: CoreContext) {
const { logger, configService } = coreContext;
this.log = logger.get('legacy-service');
this.httpConfig$ = combineLatest(
configService.atPath<HttpConfigType>(httpConfig.path),
configService.atPath<CspConfigType>(cspConfig.path),
configService.atPath<ExternalUrlConfigType>(externalUrlConfig.path)
).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl)));
}
public async setupLegacyConfig() {
this.update$ = combineLatest([
this.coreContext.configService.getConfig$(),
this.coreContext.configService.atPath<PathConfigType>('path'),
]).pipe(
tap(([config, pathConfig]) => {
if (this.kbnServer !== undefined) {
this.kbnServer.applyLoggingConfiguration(getLegacyRawConfig(config, pathConfig));
}
}),
tap({ error: (err) => this.log.error(err) }),
publishReplay(1)
) as ConnectableObservable<[Config, PathConfigType]>;
this.configSubscription = this.update$.connect();
this.settings = await this.update$
.pipe(
first(),
map(([config, pathConfig]) => getLegacyRawConfig(config, pathConfig))
)
.toPromise();
this.legacyRawConfig = LegacyConfigClass.withDefaultSchema(this.settings);
return {
settings: this.settings,
legacyConfig: this.legacyRawConfig!,
};
this.legacyLoggingConfig$ = configService.atPath<LegacyLoggingConfig>(loggingConfig.path);
this.opsConfig$ = configService.atPath<OpsConfigType>(opsConfig.path);
}
public async setup(setupDeps: LegacyServiceSetupDeps) {
this.log.debug('setting up legacy service');
if (!this.legacyRawConfig) {
throw new Error(
'Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()'
);
}
// propagate the instance uuid to the legacy config, as it was the legacy way to access it.
this.legacyRawConfig!.set('server.uuid', setupDeps.core.environment.instanceUuid);
this.setupDeps = setupDeps;
await this.setupLegacyLogging(setupDeps.http.server);
}
public async start(startDeps: LegacyServiceStartDeps) {
const { setupDeps } = this;
private async setupLegacyLogging(server: Server) {
const legacyLoggingConfig = await this.legacyLoggingConfig$.pipe(first()).toPromise();
const currentOpsConfig = await this.opsConfig$.pipe(first()).toPromise();
if (!setupDeps || !this.legacyRawConfig) {
throw new Error('Legacy service is not setup yet.');
}
await setupLogging(server, legacyLoggingConfig, currentOpsConfig.interval.asMilliseconds());
await setupLoggingRotate(server, legacyLoggingConfig);
this.log.debug('starting legacy service');
this.kbnServer = await this.createKbnServer(
this.settings!,
this.legacyRawConfig!,
setupDeps,
startDeps
this.configSubscription = combineLatest([this.legacyLoggingConfig$, this.opsConfig$]).subscribe(
([newLoggingConfig, newOpsConfig]) => {
reconfigureLogging(server, newLoggingConfig, newOpsConfig.interval.asMilliseconds());
}
);
}
@ -151,156 +71,5 @@ export class LegacyService implements CoreService {
this.configSubscription.unsubscribe();
this.configSubscription = undefined;
}
if (this.kbnServer !== undefined) {
await this.kbnServer.close();
this.kbnServer = undefined;
}
}
private async createKbnServer(
settings: LegacyVars,
config: LegacyConfig,
setupDeps: LegacyServiceSetupDeps,
startDeps: LegacyServiceStartDeps
) {
const coreStart: CoreStart = {
capabilities: startDeps.core.capabilities,
elasticsearch: startDeps.core.elasticsearch,
http: {
auth: startDeps.core.http.auth,
basePath: startDeps.core.http.basePath,
getServerInfo: startDeps.core.http.getServerInfo,
},
savedObjects: {
getScopedClient: startDeps.core.savedObjects.getScopedClient,
createScopedRepository: startDeps.core.savedObjects.createScopedRepository,
createInternalRepository: startDeps.core.savedObjects.createInternalRepository,
createSerializer: startDeps.core.savedObjects.createSerializer,
createExporter: startDeps.core.savedObjects.createExporter,
createImporter: startDeps.core.savedObjects.createImporter,
getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry,
},
metrics: {
collectionInterval: startDeps.core.metrics.collectionInterval,
getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$,
},
uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient },
coreUsageData: {
getCoreUsageData: () => {
throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy');
},
},
};
const router = setupDeps.core.http.createRouter('', this.legacyId);
const coreSetup: CoreSetup = {
capabilities: setupDeps.core.capabilities,
context: setupDeps.core.context,
elasticsearch: {
legacy: setupDeps.core.elasticsearch.legacy,
},
http: {
createCookieSessionStorageFactory: setupDeps.core.http.createCookieSessionStorageFactory,
registerRouteHandlerContext: <
Context extends RequestHandlerContext,
ContextName extends keyof Context
>(
contextName: ContextName,
provider: RequestHandlerContextProvider<Context, ContextName>
) => setupDeps.core.http.registerRouteHandlerContext(this.legacyId, contextName, provider),
createRouter: <Context extends RequestHandlerContext = RequestHandlerContext>() =>
router as IRouter<Context>,
resources: setupDeps.core.httpResources.createRegistrar(router),
registerOnPreRouting: setupDeps.core.http.registerOnPreRouting,
registerOnPreAuth: setupDeps.core.http.registerOnPreAuth,
registerAuth: setupDeps.core.http.registerAuth,
registerOnPostAuth: setupDeps.core.http.registerOnPostAuth,
registerOnPreResponse: setupDeps.core.http.registerOnPreResponse,
basePath: setupDeps.core.http.basePath,
auth: {
get: setupDeps.core.http.auth.get,
isAuthenticated: setupDeps.core.http.auth.isAuthenticated,
},
csp: setupDeps.core.http.csp,
getServerInfo: setupDeps.core.http.getServerInfo,
},
i18n: setupDeps.core.i18n,
logging: {
configure: (config$) => setupDeps.core.logging.configure([], config$),
},
metrics: {
collectionInterval: setupDeps.core.metrics.collectionInterval,
getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$,
},
savedObjects: {
setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider,
addClientWrapper: setupDeps.core.savedObjects.addClientWrapper,
registerType: setupDeps.core.savedObjects.registerType,
},
status: {
isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous,
core$: setupDeps.core.status.core$,
overall$: setupDeps.core.status.overall$,
set: () => {
throw new Error(`core.status.set is unsupported in legacy`);
},
// @ts-expect-error
get dependencies$() {
throw new Error(`core.status.dependencies$ is unsupported in legacy`);
},
// @ts-expect-error
get derivedStatus$() {
throw new Error(`core.status.derivedStatus$ is unsupported in legacy`);
},
},
uiSettings: {
register: setupDeps.core.uiSettings.register,
},
deprecations: {
registerDeprecations: () => {
throw new Error('core.setup.deprecations.registerDeprecations is unsupported in legacy');
},
},
getStartServices: () => Promise.resolve([coreStart, startDeps.plugins, {}]),
};
// eslint-disable-next-line @typescript-eslint/no-var-requires
const KbnServer = require('../../../legacy/server/kbn_server');
const kbnServer: LegacyKbnServer = new KbnServer(settings, config, {
env: {
mode: this.coreContext.env.mode,
packageInfo: this.coreContext.env.packageInfo,
},
setupDeps: {
core: coreSetup,
plugins: setupDeps.plugins,
},
startDeps: {
core: coreStart,
plugins: startDeps.plugins,
},
__internals: {
hapiServer: setupDeps.core.http.server,
uiPlugins: setupDeps.uiPlugins,
rendering: setupDeps.core.rendering,
},
logger: this.coreContext.logger,
});
const { autoListen } = await this.httpConfig$.pipe(first()).toPromise();
if (autoListen) {
try {
await kbnServer.listen();
} catch (err) {
await kbnServer.close();
throw err;
}
} else {
await kbnServer.ready();
}
return kbnServer;
}
}

View file

@ -9,11 +9,10 @@
import { schema } from '@kbn/config-schema';
import { LegacyLoggingServer } from '@kbn/legacy-logging';
import { DisposableAppender, LogRecord } from '@kbn/logging';
import { LegacyVars } from '../../types';
export interface LegacyAppenderConfig {
type: 'legacy-appender';
legacyLoggingConfig?: any;
legacyLoggingConfig?: Record<string, any>;
}
/**
@ -23,7 +22,7 @@ export interface LegacyAppenderConfig {
export class LegacyAppender implements DisposableAppender {
public static configSchema = schema.object({
type: schema.literal('legacy-appender'),
legacyLoggingConfig: schema.any(),
legacyLoggingConfig: schema.recordOf(schema.string(), schema.any()),
});
/**
@ -34,7 +33,7 @@ export class LegacyAppender implements DisposableAppender {
private readonly loggingServer: LegacyLoggingServer;
constructor(legacyLoggingConfig: Readonly<LegacyVars>) {
constructor(legacyLoggingConfig: any) {
this.loggingServer = new LegacyLoggingServer(legacyLoggingConfig);
}

View file

@ -1,188 +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 { mergeVars } from './merge_vars';
describe('mergeVars', () => {
it('merges two objects together', () => {
const first = {
otherName: 'value',
otherCanFoo: true,
otherNested: {
otherAnotherVariable: 'ok',
},
};
const second = {
name: 'value',
canFoo: true,
nested: {
anotherVariable: 'ok',
},
};
expect(mergeVars(first, second)).toEqual({
name: 'value',
canFoo: true,
nested: {
anotherVariable: 'ok',
},
otherName: 'value',
otherCanFoo: true,
otherNested: {
otherAnotherVariable: 'ok',
},
});
});
it('does not mutate the source objects', () => {
const first = {
var1: 'first',
};
const second = {
var1: 'second',
var2: 'second',
};
const third = {
var1: 'third',
var2: 'third',
var3: 'third',
};
const fourth = {
var1: 'fourth',
var2: 'fourth',
var3: 'fourth',
var4: 'fourth',
};
mergeVars(first, second, third, fourth);
expect(first).toEqual({ var1: 'first' });
expect(second).toEqual({ var1: 'second', var2: 'second' });
expect(third).toEqual({ var1: 'third', var2: 'third', var3: 'third' });
expect(fourth).toEqual({ var1: 'fourth', var2: 'fourth', var3: 'fourth', var4: 'fourth' });
});
it('merges multiple objects together with precedence increasing from left-to-right', () => {
const first = {
var1: 'first',
var2: 'first',
var3: 'first',
var4: 'first',
};
const second = {
var1: 'second',
var2: 'second',
var3: 'second',
};
const third = {
var1: 'third',
var2: 'third',
};
const fourth = {
var1: 'fourth',
};
expect(mergeVars(first, second, third, fourth)).toEqual({
var1: 'fourth',
var2: 'third',
var3: 'second',
var4: 'first',
});
});
it('overwrites the original variable value if a duplicate entry is found', () => {
const first = {
nested: {
otherAnotherVariable: 'ok',
},
};
const second = {
name: 'value',
canFoo: true,
nested: {
anotherVariable: 'ok',
},
};
expect(mergeVars(first, second)).toEqual({
name: 'value',
canFoo: true,
nested: {
anotherVariable: 'ok',
},
});
});
it('combines entries within "uiCapabilities"', () => {
const first = {
uiCapabilities: {
firstCapability: 'ok',
sharedCapability: 'shared',
},
};
const second = {
name: 'value',
canFoo: true,
uiCapabilities: {
secondCapability: 'ok',
},
};
const third = {
name: 'value',
canFoo: true,
uiCapabilities: {
thirdCapability: 'ok',
sharedCapability: 'blocked',
},
};
expect(mergeVars(first, second, third)).toEqual({
name: 'value',
canFoo: true,
uiCapabilities: {
firstCapability: 'ok',
secondCapability: 'ok',
thirdCapability: 'ok',
sharedCapability: 'blocked',
},
});
});
it('does not deeply combine entries within "uiCapabilities"', () => {
const first = {
uiCapabilities: {
firstCapability: 'ok',
nestedCapability: {
otherNestedProp: 'otherNestedValue',
},
},
};
const second = {
name: 'value',
canFoo: true,
uiCapabilities: {
secondCapability: 'ok',
nestedCapability: {
nestedProp: 'nestedValue',
},
},
};
expect(mergeVars(first, second)).toEqual({
name: 'value',
canFoo: true,
uiCapabilities: {
firstCapability: 'ok',
secondCapability: 'ok',
nestedCapability: {
nestedProp: 'nestedValue',
},
},
});
});
});

View file

@ -1,23 +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 { LegacyVars } from './types';
const ELIGIBLE_FLAT_MERGE_KEYS = ['uiCapabilities'];
export function mergeVars(...sources: LegacyVars[]): LegacyVars {
return Object.assign(
{},
...sources,
...ELIGIBLE_FLAT_MERGE_KEYS.flatMap((key) =>
sources.some((source) => key in source)
? [{ [key]: Object.assign({}, ...sources.map((source) => source[key] || {})) }]
: []
)
);
}

View file

@ -1,64 +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 { InternalCoreSetup, InternalCoreStart } from '../internal_types';
import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins';
import { InternalRenderingServiceSetup } from '../rendering';
/**
* @internal
* @deprecated
*/
export type LegacyVars = Record<string, any>;
type LegacyCoreSetup = InternalCoreSetup & {
plugins: PluginsServiceSetup;
rendering: InternalRenderingServiceSetup;
};
type LegacyCoreStart = InternalCoreStart & { plugins: PluginsServiceStart };
/**
* New platform representation of the legacy configuration (KibanaConfig)
*
* @internal
* @deprecated
*/
export interface LegacyConfig {
get<T>(key?: string): T;
has(key: string): boolean;
set(key: string, value: any): void;
set(config: LegacyVars): void;
}
/**
* @public
* @deprecated
*/
export interface LegacyServiceSetupDeps {
core: LegacyCoreSetup;
plugins: Record<string, unknown>;
uiPlugins: UiPlugins;
}
/**
* @public
* @deprecated
*/
export interface LegacyServiceStartDeps {
core: LegacyCoreStart;
plugins: Record<string, unknown>;
}
/**
* @internal
* @deprecated
*/
export interface LegacyServiceSetupConfig {
legacyConfig: LegacyConfig;
settings: LegacyVars;
}

View file

@ -1,20 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`\`schema\` creates correct schema with defaults. 1`] = `
Object {
"appenders": Map {},
"loggers": Array [],
"root": Object {
"appenders": Array [
"default",
],
"level": "info",
},
}
`;
exports[`\`schema\` throws if \`root\` logger does not have "default" appender configured. 1`] = `"[root]: \\"default\\" appender required for migration period till the next major release"`;
exports[`\`schema\` throws if \`root\` logger does not have appenders configured. 1`] = `"[root.appenders]: array size is [0], but cannot be smaller than [1]"`;
exports[`fails if loggers use unknown appenders. 1`] = `"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."`;

View file

@ -9,7 +9,35 @@
import { LoggingConfig, config } from './logging_config';
test('`schema` creates correct schema with defaults.', () => {
expect(config.schema.validate({})).toMatchSnapshot();
expect(config.schema.validate({})).toMatchInlineSnapshot(
{ json: expect.any(Boolean) }, // default value depends on TTY
`
Object {
"appenders": Map {},
"dest": "stdout",
"events": Object {},
"filter": Object {},
"json": Any<Boolean>,
"loggers": Array [],
"quiet": false,
"root": Object {
"appenders": Array [
"default",
],
"level": "info",
},
"rotate": Object {
"enabled": false,
"everyBytes": 10485760,
"keepFiles": 7,
"pollingInterval": 10000,
"usePolling": false,
},
"silent": false,
"verbose": false,
}
`
);
});
test('`schema` throws if `root` logger does not have appenders configured.', () => {
@ -19,7 +47,9 @@ test('`schema` throws if `root` logger does not have appenders configured.', ()
appenders: [],
},
})
).toThrowErrorMatchingSnapshot();
).toThrowErrorMatchingInlineSnapshot(
`"[root.appenders]: array size is [0], but cannot be smaller than [1]"`
);
});
test('`schema` throws if `root` logger does not have "default" appender configured.', () => {
@ -29,7 +59,9 @@ test('`schema` throws if `root` logger does not have "default" appender configur
appenders: ['console'],
},
})
).toThrowErrorMatchingSnapshot();
).toThrowErrorMatchingInlineSnapshot(
`"[root]: \\"default\\" appender required for migration period till the next major release"`
);
});
test('`getParentLoggerContext()` returns correct parent context name.', () => {
@ -157,7 +189,9 @@ test('fails if loggers use unknown appenders.', () => {
],
});
expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingSnapshot();
expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingInlineSnapshot(
`"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."`
);
});
describe('extend', () => {

View file

@ -7,6 +7,7 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
import { legacyLoggingConfigSchema } from '@kbn/legacy-logging';
import { AppenderConfigType, Appenders } from './appenders/appenders';
// We need this helper for the types to be correct
@ -59,7 +60,7 @@ export const loggerSchema = schema.object({
export type LoggerConfigType = TypeOf<typeof loggerSchema>;
export const config = {
path: 'logging',
schema: schema.object({
schema: legacyLoggingConfigSchema.extends({
appenders: schema.mapOf(schema.string(), Appenders.configSchema, {
defaultValue: new Map<string, AppenderConfigType>(),
}),
@ -85,7 +86,7 @@ export const config = {
}),
};
export type LoggingConfigType = Omit<TypeOf<typeof config.schema>, 'appenders'> & {
export type LoggingConfigType = Pick<TypeOf<typeof config.schema>, 'loggers' | 'root'> & {
appenders: Map<string, AppenderConfigType>;
};
@ -105,6 +106,7 @@ export const loggerContextConfigSchema = schema.object({
/** @public */
export type LoggerContextConfigType = TypeOf<typeof loggerContextConfigSchema>;
/** @public */
export interface LoggerContextConfigInput {
// config-schema knows how to handle either Maps or Records

View file

@ -16,6 +16,7 @@ jest.mock('fs', () => ({
const dynamicProps = { process: { pid: expect.any(Number) } };
jest.mock('@kbn/legacy-logging', () => ({
...(jest.requireActual('@kbn/legacy-logging') as any),
setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})),
}));

View file

@ -16,3 +16,4 @@ export type {
export type { OpsProcessMetrics, OpsServerMetrics, OpsOsMetrics } from './collectors';
export { MetricsService } from './metrics_service';
export { opsConfig } from './ops_config';
export type { OpsConfigType } from './ops_config';

View file

@ -13,7 +13,7 @@ import { getGlobalConfig, getGlobalConfig$ } from './legacy_config';
import { REPO_ROOT } from '@kbn/utils';
import { loggingSystemMock } from '../logging/logging_system.mock';
import { duration } from 'moment';
import { fromRoot } from '../utils';
import { fromRoot } from '@kbn/utils';
import { ByteSizeValue } from '@kbn/config-schema';
import { Server } from '../server';

View file

@ -9,6 +9,7 @@
import { duration } from 'moment';
import { first } from 'rxjs/operators';
import { REPO_ROOT } from '@kbn/dev-utils';
import { fromRoot } from '@kbn/utils';
import { createPluginInitializerContext, InstanceInfo } from './plugin_context';
import { CoreContext } from '../core_context';
import { Env } from '../config';
@ -16,7 +17,6 @@ import { loggingSystemMock } from '../logging/logging_system.mock';
import { rawConfigServiceMock, getEnvOptions } from '../config/mocks';
import { PluginManifest } from './types';
import { Server } from '../server';
import { fromRoot } from '../utils';
import { schema, ByteSizeValue } from '@kbn/config-schema';
import { ConfigService } from '@kbn/config';

View file

@ -7,20 +7,24 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
import { ServiceConfigDescriptor } from '../internal_types';
import { Env } from '../config';
export type PluginsConfigType = TypeOf<typeof config.schema>;
const configSchema = schema.object({
initialize: schema.boolean({ defaultValue: true }),
export const config = {
/**
* Defines an array of directories where another plugin should be loaded from.
*/
paths: schema.arrayOf(schema.string(), { defaultValue: [] }),
});
export type PluginsConfigType = TypeOf<typeof configSchema>;
export const config: ServiceConfigDescriptor<PluginsConfigType> = {
path: 'plugins',
schema: schema.object({
initialize: schema.boolean({ defaultValue: true }),
/**
* Defines an array of directories where another plugin should be loaded from.
*/
paths: schema.arrayOf(schema.string(), { defaultValue: [] }),
}),
schema: configSchema,
deprecations: ({ unusedFromRoot }) => [unusedFromRoot('plugins.scanDirs')],
};
/** @internal */

View file

@ -142,7 +142,6 @@ import { SearchParams } from 'elasticsearch';
import { SearchResponse as SearchResponse_2 } from 'elasticsearch';
import { SearchShardsParams } from 'elasticsearch';
import { SearchTemplateParams } from 'elasticsearch';
import { Server } from '@hapi/hapi';
import { ShallowPromise } from '@kbn/utility-types';
import { SnapshotCreateParams } from 'elasticsearch';
import { SnapshotCreateRepositoryParams } from 'elasticsearch';
@ -345,7 +344,7 @@ export const config: {
pingTimeout: Type<import("moment").Duration>;
logQueries: Type<boolean>;
ssl: import("@kbn/config-schema").ObjectType<{
verificationMode: Type<"certificate" | "none" | "full">;
verificationMode: Type<"none" | "certificate" | "full">;
certificateAuthorities: Type<string | string[] | undefined>;
certificate: Type<string | undefined>;
key: Type<string | undefined>;
@ -1305,10 +1304,10 @@ export type KibanaResponseFactory = typeof kibanaResponseFactory;
// @public
export const kibanaResponseFactory: {
custom: <T extends string | Record<string, any> | Error | Buffer | {
custom: <T extends string | Record<string, any> | Error | Buffer | Stream | {
message: string | Error;
attributes?: Record<string, any> | undefined;
} | Stream | undefined>(options: CustomHttpResponseOptions<T>) => KibanaResponse<T>;
} | undefined>(options: CustomHttpResponseOptions<T>) => KibanaResponse<T>;
badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse<ResponseError>;
unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse<ResponseError>;
forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse<ResponseError>;
@ -1585,20 +1584,6 @@ export class LegacyClusterClient implements ILegacyClusterClient {
close(): void;
}
// @internal @deprecated
export interface LegacyConfig {
// (undocumented)
get<T>(key?: string): T;
// (undocumented)
has(key: string): boolean;
// (undocumented)
set(key: string, value: any): void;
// Warning: (ae-forgotten-export) The symbol "LegacyVars" needs to be exported by the entry point index.d.ts
//
// (undocumented)
set(config: LegacyVars): void;
}
// @public @deprecated (undocumented)
export type LegacyElasticsearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log' | 'plugins'> & Pick<ElasticsearchConfig, 'apiVersion' | 'customHeaders' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'hosts' | 'username' | 'password'> & {
pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout'];
@ -1634,30 +1619,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient {
callAsInternalUser(endpoint: string, clientParams?: Record<string, any>, options?: LegacyCallAPIOptions): Promise<any>;
}
// @public @deprecated (undocumented)
export interface LegacyServiceSetupDeps {
// Warning: (ae-forgotten-export) The symbol "LegacyCoreSetup" needs to be exported by the entry point index.d.ts
//
// (undocumented)
core: LegacyCoreSetup;
// (undocumented)
plugins: Record<string, unknown>;
// Warning: (ae-forgotten-export) The symbol "UiPlugins" needs to be exported by the entry point index.d.ts
//
// (undocumented)
uiPlugins: UiPlugins;
}
// @public @deprecated (undocumented)
export interface LegacyServiceStartDeps {
// Warning: (ae-forgotten-export) The symbol "LegacyCoreStart" needs to be exported by the entry point index.d.ts
//
// (undocumented)
core: LegacyCoreStart;
// (undocumented)
plugins: Record<string, unknown>;
}
// Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts
//
// @public

View file

@ -58,7 +58,7 @@ jest.doMock('./ui_settings/ui_settings_service', () => ({
}));
export const mockEnsureValidConfiguration = jest.fn();
jest.doMock('./legacy/config/ensure_valid_configuration', () => ({
jest.doMock('./config/ensure_valid_configuration', () => ({
ensureValidConfiguration: mockEnsureValidConfiguration,
}));

View file

@ -99,7 +99,6 @@ test('injects legacy dependency to context#setup()', async () => {
pluginDependencies: new Map([
[pluginA, []],
[pluginB, [pluginA]],
[mockLegacyService.legacyId, [pluginA, pluginB]],
]),
});
});
@ -108,12 +107,10 @@ test('runs services on "start"', async () => {
const server = new Server(rawConfigService, env, logger);
expect(mockHttpService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.start).not.toHaveBeenCalled();
await server.setup();
expect(mockHttpService.start).not.toHaveBeenCalled();
expect(mockLegacyService.start).not.toHaveBeenCalled();
expect(mockSavedObjectsService.start).not.toHaveBeenCalled();
expect(mockUiSettingsService.start).not.toHaveBeenCalled();
expect(mockMetricsService.start).not.toHaveBeenCalled();
@ -121,7 +118,6 @@ test('runs services on "start"', async () => {
await server.start();
expect(mockHttpService.start).toHaveBeenCalledTimes(1);
expect(mockLegacyService.start).toHaveBeenCalledTimes(1);
expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1);
expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1);
expect(mockMetricsService.start).toHaveBeenCalledTimes(1);
@ -164,26 +160,6 @@ test('stops services on "stop"', async () => {
});
test(`doesn't setup core services if config validation fails`, async () => {
mockConfigService.validate.mockImplementationOnce(() => {
return Promise.reject(new Error('invalid config'));
});
const server = new Server(rawConfigService, env, logger);
await expect(server.setup()).rejects.toThrowErrorMatchingInlineSnapshot(`"invalid config"`);
expect(mockHttpService.setup).not.toHaveBeenCalled();
expect(mockElasticsearchService.setup).not.toHaveBeenCalled();
expect(mockPluginsService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.setup).not.toHaveBeenCalled();
expect(mockSavedObjectsService.stop).not.toHaveBeenCalled();
expect(mockUiSettingsService.setup).not.toHaveBeenCalled();
expect(mockRenderingService.setup).not.toHaveBeenCalled();
expect(mockMetricsService.setup).not.toHaveBeenCalled();
expect(mockStatusService.setup).not.toHaveBeenCalled();
expect(mockLoggingService.setup).not.toHaveBeenCalled();
expect(mockI18nService.setup).not.toHaveBeenCalled();
});
test(`doesn't setup core services if legacy config validation fails`, async () => {
mockEnsureValidConfiguration.mockImplementation(() => {
throw new Error('Unknown configuration keys');
});

View file

@ -8,15 +8,20 @@
import apm from 'elastic-apm-node';
import { config as pathConfig } from '@kbn/utils';
import { mapToObject } from '@kbn/std';
import { ConfigService, Env, RawConfigurationProvider, coreDeprecationProvider } from './config';
import {
ConfigService,
Env,
RawConfigurationProvider,
coreDeprecationProvider,
ensureValidConfiguration,
} from './config';
import { CoreApp } from './core_app';
import { I18nService } from './i18n';
import { ElasticsearchService } from './elasticsearch';
import { HttpService } from './http';
import { HttpResourcesService } from './http_resources';
import { RenderingService } from './rendering';
import { LegacyService, ensureValidConfiguration } from './legacy';
import { LegacyService } from './legacy';
import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging';
import { UiSettingsService } from './ui_settings';
import { PluginsService, config as pluginsConfig } from './plugins';
@ -121,22 +126,13 @@ export class Server {
const { pluginTree, pluginPaths, uiPlugins } = await this.plugins.discover({
environment: environmentSetup,
});
const legacyConfigSetup = await this.legacy.setupLegacyConfig();
// Immediately terminate in case of invalid configuration
// This needs to be done after plugin discovery
await this.configService.validate();
await ensureValidConfiguration(this.configService, legacyConfigSetup);
await ensureValidConfiguration(this.configService);
const contextServiceSetup = this.context.setup({
// We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins:
// 1) Can access context from any KP plugin
// 2) Can register context providers that will only be available to other legacy plugins and will not leak into
// New Platform plugins.
pluginDependencies: new Map([
...pluginTree.asOpaqueIds,
[this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]],
]),
pluginDependencies: new Map([...pluginTree.asOpaqueIds]),
});
const httpSetup = await this.http.setup({
@ -222,9 +218,7 @@ export class Server {
this.#pluginsInitialized = pluginsSetup.initialized;
await this.legacy.setup({
core: { ...coreSetup, plugins: pluginsSetup, rendering: renderingSetup },
plugins: mapToObject(pluginsSetup.contracts),
uiPlugins,
http: httpSetup,
});
this.registerCoreContext(coreSetup);
@ -266,15 +260,7 @@ export class Server {
coreUsageData: coreUsageDataStart,
};
const pluginsStart = await this.plugins.start(this.coreStart);
await this.legacy.start({
core: {
...this.coreStart,
plugins: pluginsStart,
},
plugins: mapToObject(pluginsStart.contracts),
});
await this.plugins.start(this.coreStart);
await this.http.start();

View file

@ -39,6 +39,5 @@ export type {
} from './saved_objects/types';
export type { DomainDeprecationDetails, DeprecationsGetResponse } from './deprecations/types';
export * from './ui_settings/types';
export * from './legacy/types';
export type { EnvironmentMode, PackageInfo } from '@kbn/config';
export type { ExternalUrlConfig, IExternalUrlPolicy } from './external_url';

View file

@ -9,10 +9,10 @@
import { getServices, chance } from './lib';
export const docExistsSuite = (savedObjectsIndex: string) => () => {
async function setup(options: any = {}) {
async function setup(options: { initialSettings?: Record<string, any> } = {}) {
const { initialSettings } = options;
const { kbnServer, uiSettings, callCluster } = getServices();
const { uiSettings, callCluster, supertest } = getServices();
// delete the kibana index to ensure we start fresh
await callCluster('deleteByQuery', {
@ -21,31 +21,30 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => {
conflicts: 'proceed',
query: { match_all: {} },
},
refresh: true,
wait_for_completion: true,
});
if (initialSettings) {
await uiSettings.setMany(initialSettings);
}
return { kbnServer, uiSettings };
return { uiSettings, supertest };
}
describe('get route', () => {
it('returns a 200 and includes userValues', async () => {
const defaultIndex = chance.word({ length: 10 });
const { kbnServer } = await setup({
const { supertest } = await setup({
initialSettings: {
defaultIndex,
},
});
const { statusCode, result } = await kbnServer.inject({
method: 'GET',
url: '/api/kibana/settings',
});
const { body } = await supertest('get', '/api/kibana/settings').expect(200);
expect(statusCode).toBe(200);
expect(result).toMatchObject({
expect(body).toMatchObject({
settings: {
buildNum: {
userValue: expect.any(Number),
@ -64,20 +63,17 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => {
describe('set route', () => {
it('returns a 200 and all values including update', async () => {
const { kbnServer } = await setup();
const { supertest } = await setup();
const defaultIndex = chance.word();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings/defaultIndex',
payload: {
const { body } = await supertest('post', '/api/kibana/settings/defaultIndex')
.send({
value: defaultIndex,
},
});
})
.expect(200);
expect(statusCode).toBe(200);
expect(result).toMatchObject({
expect(body).toMatchObject({
settings: {
buildNum: {
userValue: expect.any(Number),
@ -94,18 +90,15 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => {
});
it('returns a 400 if trying to set overridden value', async () => {
const { kbnServer } = await setup();
const { supertest } = await setup();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings/foo',
payload: {
const { body } = await supertest('delete', '/api/kibana/settings/foo')
.send({
value: 'baz',
},
});
})
.expect(400);
expect(statusCode).toBe(400);
expect(result).toEqual({
expect(body).toEqual({
error: 'Bad Request',
message: 'Unable to update "foo" because it is overridden',
statusCode: 400,
@ -115,22 +108,18 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => {
describe('setMany route', () => {
it('returns a 200 and all values including updates', async () => {
const { kbnServer } = await setup();
const { supertest } = await setup();
const defaultIndex = chance.word();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings',
payload: {
const { body } = await supertest('post', '/api/kibana/settings')
.send({
changes: {
defaultIndex,
},
},
});
})
.expect(200);
expect(statusCode).toBe(200);
expect(result).toMatchObject({
expect(body).toMatchObject({
settings: {
buildNum: {
userValue: expect.any(Number),
@ -147,20 +136,17 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => {
});
it('returns a 400 if trying to set overridden value', async () => {
const { kbnServer } = await setup();
const { supertest } = await setup();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings',
payload: {
const { body } = await supertest('post', '/api/kibana/settings')
.send({
changes: {
foo: 'baz',
},
},
});
})
.expect(400);
expect(statusCode).toBe(400);
expect(result).toEqual({
expect(body).toEqual({
error: 'Bad Request',
message: 'Unable to update "foo" because it is overridden',
statusCode: 400,
@ -172,19 +158,15 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => {
it('returns a 200 and deletes the setting', async () => {
const defaultIndex = chance.word({ length: 10 });
const { kbnServer, uiSettings } = await setup({
const { uiSettings, supertest } = await setup({
initialSettings: { defaultIndex },
});
expect(await uiSettings.get('defaultIndex')).toBe(defaultIndex);
const { statusCode, result } = await kbnServer.inject({
method: 'DELETE',
url: '/api/kibana/settings/defaultIndex',
});
const { body } = await supertest('delete', '/api/kibana/settings/defaultIndex').expect(200);
expect(statusCode).toBe(200);
expect(result).toMatchObject({
expect(body).toMatchObject({
settings: {
buildNum: {
userValue: expect.any(Number),
@ -197,15 +179,11 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => {
});
});
it('returns a 400 if deleting overridden value', async () => {
const { kbnServer } = await setup();
const { supertest } = await setup();
const { statusCode, result } = await kbnServer.inject({
method: 'DELETE',
url: '/api/kibana/settings/foo',
});
const { body } = await supertest('delete', '/api/kibana/settings/foo').expect(400);
expect(statusCode).toBe(400);
expect(result).toEqual({
expect(body).toEqual({
error: 'Bad Request',
message: 'Unable to update "foo" because it is overridden',
statusCode: 400,

View file

@ -11,14 +11,7 @@ import { getServices, chance } from './lib';
export const docMissingSuite = (savedObjectsIndex: string) => () => {
// ensure the kibana index has no documents
beforeEach(async () => {
const { kbnServer, callCluster } = getServices();
// write a setting to ensure kibana index is created
await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings/defaultIndex',
payload: { value: 'abc' },
});
const { callCluster } = getServices();
// delete all docs from kibana index to ensure savedConfig is not found
await callCluster('deleteByQuery', {
@ -31,15 +24,11 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => {
describe('get route', () => {
it('creates doc, returns a 200 with settings', async () => {
const { kbnServer } = getServices();
const { supertest } = getServices();
const { statusCode, result } = await kbnServer.inject({
method: 'GET',
url: '/api/kibana/settings',
});
const { body } = await supertest('get', '/api/kibana/settings').expect(200);
expect(statusCode).toBe(200);
expect(result).toMatchObject({
expect(body).toMatchObject({
settings: {
buildNum: {
userValue: expect.any(Number),
@ -55,17 +44,17 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => {
describe('set route', () => {
it('creates doc, returns a 200 with value set', async () => {
const { kbnServer } = getServices();
const { supertest } = getServices();
const defaultIndex = chance.word();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings/defaultIndex',
payload: { value: defaultIndex },
});
expect(statusCode).toBe(200);
expect(result).toMatchObject({
const { body } = await supertest('post', '/api/kibana/settings/defaultIndex')
.send({
value: defaultIndex,
})
.expect(200);
expect(body).toMatchObject({
settings: {
buildNum: {
userValue: expect.any(Number),
@ -84,19 +73,17 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => {
describe('setMany route', () => {
it('creates doc, returns 200 with updated values', async () => {
const { kbnServer } = getServices();
const { supertest } = getServices();
const defaultIndex = chance.word();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings',
payload: {
changes: { defaultIndex },
},
});
expect(statusCode).toBe(200);
expect(result).toMatchObject({
const { body } = await supertest('post', '/api/kibana/settings')
.send({
changes: { defaultIndex },
})
.expect(200);
expect(body).toMatchObject({
settings: {
buildNum: {
userValue: expect.any(Number),
@ -115,15 +102,11 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => {
describe('delete route', () => {
it('creates doc, returns a 200 with just buildNum', async () => {
const { kbnServer } = getServices();
const { supertest } = getServices();
const { statusCode, result } = await kbnServer.inject({
method: 'DELETE',
url: '/api/kibana/settings/defaultIndex',
});
const { body } = await supertest('delete', '/api/kibana/settings/defaultIndex').expect(200);
expect(statusCode).toBe(200);
expect(result).toMatchObject({
expect(body).toMatchObject({
settings: {
buildNum: {
userValue: expect.any(Number),

View file

@ -1,145 +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 { getServices, chance } from './lib';
export const docMissingAndIndexReadOnlySuite = (savedObjectsIndex: string) => () => {
// ensure the kibana index has no documents
beforeEach(async () => {
const { kbnServer, callCluster } = getServices();
// write a setting to ensure kibana index is created
await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings/defaultIndex',
payload: { value: 'abc' },
});
// delete all docs from kibana index to ensure savedConfig is not found
await callCluster('deleteByQuery', {
index: savedObjectsIndex,
body: {
query: { match_all: {} },
},
});
// set the index to read only
await callCluster('indices.putSettings', {
index: savedObjectsIndex,
body: {
index: {
blocks: {
read_only: true,
},
},
},
});
});
afterEach(async () => {
const { callCluster } = getServices();
// disable the read only block
await callCluster('indices.putSettings', {
index: savedObjectsIndex,
body: {
index: {
blocks: {
read_only: false,
},
},
},
});
});
describe('get route', () => {
it('returns simulated doc with buildNum', async () => {
const { kbnServer } = getServices();
const { statusCode, result } = await kbnServer.inject({
method: 'GET',
url: '/api/kibana/settings',
});
expect(statusCode).toBe(200);
expect(result).toMatchObject({
settings: {
buildNum: {
userValue: expect.any(Number),
},
foo: {
userValue: 'bar',
isOverridden: true,
},
},
});
});
});
describe('set route', () => {
it('fails with 403 forbidden', async () => {
const { kbnServer } = getServices();
const defaultIndex = chance.word();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings/defaultIndex',
payload: { value: defaultIndex },
});
expect(statusCode).toBe(403);
expect(result).toEqual({
error: 'Forbidden',
message: expect.stringContaining('index read-only'),
statusCode: 403,
});
});
});
describe('setMany route', () => {
it('fails with 403 forbidden', async () => {
const { kbnServer } = getServices();
const defaultIndex = chance.word();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings',
payload: {
changes: { defaultIndex },
},
});
expect(statusCode).toBe(403);
expect(result).toEqual({
error: 'Forbidden',
message: expect.stringContaining('index read-only'),
statusCode: 403,
});
});
});
describe('delete route', () => {
it('fails with 403 forbidden', async () => {
const { kbnServer } = getServices();
const { statusCode, result } = await kbnServer.inject({
method: 'DELETE',
url: '/api/kibana/settings/defaultIndex',
});
expect(statusCode).toBe(403);
expect(result).toEqual({
error: 'Forbidden',
message: expect.stringContaining('index read-only'),
statusCode: 403,
});
});
});
};

View file

@ -12,7 +12,6 @@ import { getEnvOptions } from '@kbn/config/target/mocks';
import { startServers, stopServers } from './lib';
import { docExistsSuite } from './doc_exists';
import { docMissingSuite } from './doc_missing';
import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only';
const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version;
const savedObjectIndex = `.kibana_${kibanaVersion}_001`;
@ -23,7 +22,6 @@ describe('uiSettings/routes', function () {
beforeAll(startServers);
/* eslint-disable jest/valid-describe */
describe('doc missing', docMissingSuite(savedObjectIndex));
describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite(savedObjectIndex));
describe('doc exists', docExistsSuite(savedObjectIndex));
/* eslint-enable jest/valid-describe */
afterAll(stopServers);

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type supertest from 'supertest';
import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/server';
import {
@ -13,6 +14,8 @@ import {
TestElasticsearchUtils,
TestKibanaUtils,
TestUtils,
HttpMethod,
getSupertest,
} from '../../../../test_helpers/kbn_server';
import { LegacyAPICaller } from '../../../elasticsearch/';
import { httpServerMock } from '../../../http/http_server.mocks';
@ -21,13 +24,11 @@ let servers: TestUtils;
let esServer: TestElasticsearchUtils;
let kbn: TestKibanaUtils;
let kbnServer: TestKibanaUtils['kbnServer'];
interface AllServices {
kbnServer: TestKibanaUtils['kbnServer'];
savedObjectsClient: SavedObjectsClientContract;
callCluster: LegacyAPICaller;
uiSettings: IUiSettingsClient;
supertest: (method: HttpMethod, path: string) => supertest.Test;
}
let services: AllServices;
@ -47,7 +48,6 @@ export async function startServers() {
});
esServer = await servers.startES();
kbn = await servers.startKibana();
kbnServer = kbn.kbnServer;
}
export function getServices() {
@ -61,12 +61,10 @@ export function getServices() {
httpServerMock.createKibanaRequest()
);
const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(
savedObjectsClient
);
const uiSettings = kbn.coreStart.uiSettings.asScopedToClient(savedObjectsClient);
services = {
kbnServer,
supertest: (method: HttpMethod, path: string) => getSupertest(kbn.root, method, path),
callCluster,
savedObjectsClient,
uiSettings,
@ -77,7 +75,6 @@ export function getServices() {
export async function stopServers() {
services = null!;
kbnServer = null!;
if (servers) {
await esServer.stop();
await kbn.stop();

View file

@ -1,14 +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 { resolve } from 'path';
import { pkg } from './package_json';
export function fromRoot(...args: string[]) {
return resolve(pkg.__dirname, ...args);
}

View file

@ -1,10 +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.
*/
export * from './from_root';
export * from './package_json';

View file

@ -1,15 +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 { dirname } from 'path';
export const pkg = {
__filename: require.resolve('../../../../package.json'),
__dirname: dirname(require.resolve('../../../../package.json')),
...require('../../../../package.json'),
};

View file

@ -29,11 +29,10 @@ import { resolve } from 'path';
import { BehaviorSubject } from 'rxjs';
import supertest from 'supertest';
import { CoreStart } from 'src/core/server';
import { InternalCoreSetup, InternalCoreStart } from '../server/internal_types';
import { LegacyAPICaller } from '../server/elasticsearch';
import { CliArgs, Env } from '../server/config';
import { Root } from '../server/root';
import KbnServer from '../../legacy/server/kbn_server';
export type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put';
@ -125,14 +124,6 @@ export function createRootWithCorePlugins(settings = {}, cliArgs: Partial<CliArg
);
}
/**
* Returns `kbnServer` instance used in the "legacy" Kibana.
* @param root
*/
export function getKbnServer(root: Root): KbnServer {
return (root as any).server.legacy.kbnServer;
}
export const request: Record<
HttpMethod,
(root: Root, path: string) => ReturnType<typeof getSupertest>
@ -164,8 +155,8 @@ export interface TestElasticsearchUtils {
export interface TestKibanaUtils {
root: Root;
coreStart: CoreStart;
kbnServer: KbnServer;
coreSetup: InternalCoreSetup;
coreStart: InternalCoreStart;
stop: () => Promise<void>;
}
@ -283,14 +274,12 @@ export function createTestServers({
startKibana: async () => {
const root = createRootWithCorePlugins(kbnSettings);
await root.setup();
const coreSetup = await root.setup();
const coreStart = await root.start();
const kbnServer = getKbnServer(root);
return {
root,
kbnServer,
coreSetup,
coreStart,
stop: async () => await root.shutdown(),
};

View file

@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib/config/config class Config() #getDefault(key) array key should throw exception for unknown key 1`] = `"Unknown config key: foo,bar."`;
exports[`lib/config/config class Config() #getDefault(key) dot notation key should throw exception for unknown key 1`] = `"Unknown config key: foo.bar."`;

View file

@ -1,207 +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 Joi from 'joi';
import { set } from '@elastic/safer-lodash-set';
import _ from 'lodash';
import { override } from './override';
import createDefaultSchema from './schema';
import { unset, deepCloneWithBuffers as clone, IS_KIBANA_DISTRIBUTABLE } from '../../utils';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { pkg } from '../../../core/server/utils';
const schema = Symbol('Joi Schema');
const schemaExts = Symbol('Schema Extensions');
const vals = Symbol('config values');
export class Config {
static withDefaultSchema(settings = {}) {
const defaultSchema = createDefaultSchema();
return new Config(defaultSchema, settings);
}
constructor(initialSchema, initialSettings) {
this[schemaExts] = Object.create(null);
this[vals] = Object.create(null);
this.extendSchema(initialSchema, initialSettings);
}
extendSchema(extension, settings, key) {
if (!extension) {
return;
}
if (!key) {
return _.each(extension._inner.children, (child) => {
this.extendSchema(child.schema, _.get(settings, child.key), child.key);
});
}
if (this.has(key)) {
throw new Error(`Config schema already has key: ${key}`);
}
set(this[schemaExts], key, extension);
this[schema] = null;
this.set(key, settings);
}
removeSchema(key) {
if (!_.has(this[schemaExts], key)) {
throw new TypeError(`Unknown schema key: ${key}`);
}
this[schema] = null;
unset(this[schemaExts], key);
unset(this[vals], key);
}
resetTo(obj) {
this._commit(obj);
}
set(key, value) {
// clone and modify the config
let config = clone(this[vals]);
if (_.isPlainObject(key)) {
config = override(config, key);
} else {
set(config, key, value);
}
// attempt to validate the config value
this._commit(config);
}
_commit(newVals) {
// resolve the current environment
let env = newVals.env;
delete newVals.env;
if (_.isObject(env)) env = env.name;
if (!env) env = 'production';
const dev = env === 'development';
const prod = env === 'production';
// pass the environment as context so that it can be refed in config
const context = {
env: env,
prod: prod,
dev: dev,
notProd: !prod,
notDev: !dev,
version: _.get(pkg, 'version'),
branch: _.get(pkg, 'branch'),
buildNum: IS_KIBANA_DISTRIBUTABLE ? pkg.build.number : Number.MAX_SAFE_INTEGER,
buildSha: IS_KIBANA_DISTRIBUTABLE
? pkg.build.sha
: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
dist: IS_KIBANA_DISTRIBUTABLE,
};
if (!context.dev && !context.prod) {
throw new TypeError(
`Unexpected environment "${env}", expected one of "development" or "production"`
);
}
const results = Joi.validate(newVals, this.getSchema(), {
context,
abortEarly: false,
});
if (results.error) {
const error = new Error(results.error.message);
error.name = results.error.name;
error.stack = results.error.stack;
throw error;
}
this[vals] = results.value;
}
get(key) {
if (!key) {
return clone(this[vals]);
}
const value = _.get(this[vals], key);
if (value === undefined) {
if (!this.has(key)) {
throw new Error('Unknown config key: ' + key);
}
}
return clone(value);
}
getDefault(key) {
const schemaKey = Array.isArray(key) ? key.join('.') : key;
const subSchema = Joi.reach(this.getSchema(), schemaKey);
if (!subSchema) {
throw new Error(`Unknown config key: ${key}.`);
}
return clone(_.get(Joi.describe(subSchema), 'flags.default'));
}
has(key) {
function has(key, schema, path) {
path = path || [];
// Catch the partial paths
if (path.join('.') === key) return true;
// Only go deep on inner objects with children
if (_.size(schema._inner.children)) {
for (let i = 0; i < schema._inner.children.length; i++) {
const child = schema._inner.children[i];
// If the child is an object recurse through it's children and return
// true if there's a match
if (child.schema._type === 'object') {
if (has(key, child.schema, path.concat([child.key]))) return true;
// if the child matches, return true
} else if (path.concat([child.key]).join('.') === key) {
return true;
}
}
}
}
if (Array.isArray(key)) {
// TODO: add .has() support for array keys
key = key.join('.');
}
return !!has(key, this.getSchema());
}
getSchema() {
if (!this[schema]) {
this[schema] = (function convertToSchema(children) {
let schema = Joi.object().keys({}).default();
for (const key of Object.keys(children)) {
const child = children[key];
const childSchema = _.isPlainObject(child) ? convertToSchema(child) : child;
if (!childSchema || !childSchema.isJoi) {
throw new TypeError(
'Unable to convert configuration definition value to Joi schema: ' + childSchema
);
}
schema = schema.keys({ [key]: childSchema });
}
return schema;
})(this[schemaExts]);
}
return this[schema];
}
}

View file

@ -1,345 +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 { Config } from './config';
import _ from 'lodash';
import Joi from 'joi';
/**
* Plugins should defined a config method that takes a joi object. By default
* it should return a way to disallow config
*
* Config should be newed up with a joi schema (containing defaults via joi)
*
* let schema = { ... }
* new Config(schema);
*
*/
const data = {
test: {
hosts: ['host-01', 'host-02'],
client: {
type: 'datastore',
host: 'store-01',
port: 5050,
},
},
};
const schema = Joi.object({
test: Joi.object({
enable: Joi.boolean().default(true),
hosts: Joi.array().items(Joi.string()),
client: Joi.object({
type: Joi.string().default('datastore'),
host: Joi.string(),
port: Joi.number(),
}).default(),
undefValue: Joi.string(),
}).default(),
}).default();
describe('lib/config/config', function () {
describe('class Config()', function () {
describe('constructor', function () {
it('should not allow any config if the schema is not passed', function () {
const config = new Config();
const run = function () {
config.set('something.enable', true);
};
expect(run).toThrow();
});
it('should allow keys in the schema', function () {
const config = new Config(schema);
const run = function () {
config.set('test.client.host', 'http://localhost');
};
expect(run).not.toThrow();
});
it('should not allow keys not in the schema', function () {
const config = new Config(schema);
const run = function () {
config.set('paramNotDefinedInTheSchema', true);
};
expect(run).toThrow();
});
it('should not allow child keys not in the schema', function () {
const config = new Config(schema);
const run = function () {
config.set('test.client.paramNotDefinedInTheSchema', true);
};
expect(run).toThrow();
});
it('should set defaults', function () {
const config = new Config(schema);
expect(config.get('test.enable')).toBe(true);
expect(config.get('test.client.type')).toBe('datastore');
});
});
describe('#resetTo(object)', function () {
let config;
beforeEach(function () {
config = new Config(schema);
});
it('should reset the config object with new values', function () {
config.set(data);
const newData = config.get();
newData.test.enable = false;
config.resetTo(newData);
expect(config.get()).toEqual(newData);
});
});
describe('#has(key)', function () {
let config;
beforeEach(function () {
config = new Config(schema);
});
it('should return true for fields that exist in the schema', function () {
expect(config.has('test.undefValue')).toBe(true);
});
it('should return true for partial objects that exist in the schema', function () {
expect(config.has('test.client')).toBe(true);
});
it('should return false for fields that do not exist in the schema', function () {
expect(config.has('test.client.pool')).toBe(false);
});
});
describe('#set(key, value)', function () {
let config;
beforeEach(function () {
config = new Config(schema);
});
it('should use a key and value to set a config value', function () {
config.set('test.enable', false);
expect(config.get('test.enable')).toBe(false);
});
it('should use an object to set config values', function () {
const hosts = ['host-01', 'host-02'];
config.set({ test: { enable: false, hosts: hosts } });
expect(config.get('test.enable')).toBe(false);
expect(config.get('test.hosts')).toEqual(hosts);
});
it('should use a flatten object to set config values', function () {
const hosts = ['host-01', 'host-02'];
config.set({ 'test.enable': false, 'test.hosts': hosts });
expect(config.get('test.enable')).toBe(false);
expect(config.get('test.hosts')).toEqual(hosts);
});
it('should override values with just the values present', function () {
const newData = _.cloneDeep(data);
config.set(data);
newData.test.enable = false;
config.set({ test: { enable: false } });
expect(config.get()).toEqual(newData);
});
it('should thow an exception when setting a value with the wrong type', function (done) {
expect.assertions(4);
const run = function () {
config.set('test.enable', 'something');
};
try {
run();
} catch (err) {
expect(err).toHaveProperty('name', 'ValidationError');
expect(err).toHaveProperty(
'message',
'child "test" fails because [child "enable" fails because ["enable" must be a boolean]]'
);
expect(err).not.toHaveProperty('details');
expect(err).not.toHaveProperty('_object');
}
done();
});
});
describe('#get(key)', function () {
let config;
beforeEach(function () {
config = new Config(schema);
config.set(data);
});
it('should return the whole config object when called without a key', function () {
const newData = _.cloneDeep(data);
newData.test.enable = true;
expect(config.get()).toEqual(newData);
});
it('should return the value using dot notation', function () {
expect(config.get('test.enable')).toBe(true);
});
it('should return the clone of partial object using dot notation', function () {
expect(config.get('test.client')).not.toBe(data.test.client);
expect(config.get('test.client')).toEqual(data.test.client);
});
it('should throw exception for unknown config values', function () {
const run = function () {
config.get('test.does.not.exist');
};
expect(run).toThrowError(/Unknown config key: test.does.not.exist/);
});
it('should not throw exception for undefined known config values', function () {
const run = function getUndefValue() {
config.get('test.undefValue');
};
expect(run).not.toThrow();
});
});
describe('#getDefault(key)', function () {
let config;
beforeEach(function () {
config = new Config(schema);
config.set(data);
});
describe('dot notation key', function () {
it('should return undefined if there is no default', function () {
const hostDefault = config.getDefault('test.client.host');
expect(hostDefault).toBeUndefined();
});
it('should return default if specified', function () {
const typeDefault = config.getDefault('test.client.type');
expect(typeDefault).toBe('datastore');
});
it('should throw exception for unknown key', function () {
expect(() => {
config.getDefault('foo.bar');
}).toThrowErrorMatchingSnapshot();
});
});
describe('array key', function () {
it('should return undefined if there is no default', function () {
const hostDefault = config.getDefault(['test', 'client', 'host']);
expect(hostDefault).toBeUndefined();
});
it('should return default if specified', function () {
const typeDefault = config.getDefault(['test', 'client', 'type']);
expect(typeDefault).toBe('datastore');
});
it('should throw exception for unknown key', function () {
expect(() => {
config.getDefault(['foo', 'bar']);
}).toThrowErrorMatchingSnapshot();
});
});
it('object schema with no default should return default value for property', function () {
const noDefaultSchema = Joi.object()
.keys({
foo: Joi.array().items(Joi.string().min(1)).default(['bar']),
})
.required();
const config = new Config(noDefaultSchema);
config.set({
foo: ['baz'],
});
const fooDefault = config.getDefault('foo');
expect(fooDefault).toEqual(['bar']);
});
it('should return clone of the default', function () {
const schemaWithArrayDefault = Joi.object()
.keys({
foo: Joi.array().items(Joi.string().min(1)).default(['bar']),
})
.default();
const config = new Config(schemaWithArrayDefault);
config.set({
foo: ['baz'],
});
expect(config.getDefault('foo')).not.toBe(config.getDefault('foo'));
expect(config.getDefault('foo')).toEqual(config.getDefault('foo'));
});
});
describe('#extendSchema(key, schema)', function () {
let config;
beforeEach(function () {
config = new Config(schema);
});
it('should allow you to extend the schema at the top level', function () {
const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
config.extendSchema(newSchema, {}, 'myTest');
expect(config.get('myTest.test')).toBe(true);
});
it('should allow you to extend the schema with a prefix', function () {
const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
config.extendSchema(newSchema, {}, 'prefix.myTest');
expect(config.get('prefix')).toEqual({ myTest: { test: true } });
expect(config.get('prefix.myTest')).toEqual({ test: true });
expect(config.get('prefix.myTest.test')).toBe(true);
});
it('should NOT allow you to extend the schema if something else is there', function () {
const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
const run = function () {
config.extendSchema('test', newSchema);
};
expect(run).toThrow();
});
});
describe('#removeSchema(key)', function () {
it('should completely remove the key', function () {
const config = new Config(
Joi.object().keys({
a: Joi.number().default(1),
})
);
expect(config.get('a')).toBe(1);
config.removeSchema('a');
expect(() => config.get('a')).toThrowError('Unknown config key');
});
it('only removes existing keys', function () {
const config = new Config(Joi.object());
expect(() => config.removeSchema('b')).toThrowError('Unknown schema');
});
});
});
});

View file

@ -1,9 +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.
*/
export { Config } from './config';

View file

@ -1,119 +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 { override } from './override';
describe('override(target, source)', function () {
it('should override the values form source to target', function () {
const target = {
test: {
enable: true,
host: ['something else'],
client: {
type: 'sql',
},
},
};
const source = {
test: {
host: ['host-01', 'host-02'],
client: {
type: 'nosql',
},
foo: {
bar: {
baz: 1,
},
},
},
};
expect(override(target, source)).toMatchInlineSnapshot(`
Object {
"test": Object {
"client": Object {
"type": "nosql",
},
"enable": true,
"foo": Object {
"bar": Object {
"baz": 1,
},
},
"host": Array [
"host-01",
"host-02",
],
},
}
`);
});
it('does not mutate arguments', () => {
const target = {
foo: {
bar: 1,
baz: 1,
},
};
const source = {
foo: {
bar: 2,
},
box: 2,
};
expect(override(target, source)).toMatchInlineSnapshot(`
Object {
"box": 2,
"foo": Object {
"bar": 2,
"baz": 1,
},
}
`);
expect(target).not.toHaveProperty('box');
expect(source.foo).not.toHaveProperty('baz');
});
it('explodes keys with dots in them', () => {
const target = {
foo: {
bar: 1,
},
'baz.box.boot.bar.bar': 20,
};
const source = {
'foo.bar': 2,
'baz.box.boot': {
'bar.foo': 10,
},
};
expect(override(target, source)).toMatchInlineSnapshot(`
Object {
"baz": Object {
"box": Object {
"boot": Object {
"bar": Object {
"bar": 20,
"foo": 10,
},
},
},
},
"foo": Object {
"bar": 2,
},
}
`);
});
});

View file

@ -1,41 +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.
*/
const isObject = (v: any): v is Record<string, any> =>
typeof v === 'object' && v !== null && !Array.isArray(v);
const assignDeep = (target: Record<string, any>, source: Record<string, any>) => {
for (let [key, value] of Object.entries(source)) {
// unwrap dot-separated keys
if (key.includes('.')) {
const [first, ...others] = key.split('.');
key = first;
value = { [others.join('.')]: value };
}
if (isObject(value)) {
if (!target.hasOwnProperty(key)) {
target[key] = {};
}
assignDeep(target[key], value);
} else {
target[key] = value;
}
}
};
export const override = (...sources: Array<Record<string, any>>): Record<string, any> => {
const result = {};
for (const object of sources) {
assignDeep(result, object);
}
return result;
};

View file

@ -1,95 +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 Joi from 'joi';
import os from 'os';
import { legacyLoggingConfigSchema } from '@kbn/legacy-logging';
const HANDLED_IN_NEW_PLATFORM = Joi.any().description(
'This key is handled in the new platform ONLY'
);
export default () =>
Joi.object({
elastic: Joi.object({
apm: HANDLED_IN_NEW_PLATFORM,
}).default(),
pkg: Joi.object({
version: Joi.string().default(Joi.ref('$version')),
branch: Joi.string().default(Joi.ref('$branch')),
buildNum: Joi.number().default(Joi.ref('$buildNum')),
buildSha: Joi.string().default(Joi.ref('$buildSha')),
}).default(),
env: Joi.object({
name: Joi.string().default(Joi.ref('$env')),
dev: Joi.boolean().default(Joi.ref('$dev')),
prod: Joi.boolean().default(Joi.ref('$prod')),
}).default(),
dev: HANDLED_IN_NEW_PLATFORM,
pid: HANDLED_IN_NEW_PLATFORM,
csp: HANDLED_IN_NEW_PLATFORM,
server: Joi.object({
name: Joi.string().default(os.hostname()),
// keep them for BWC, remove when not used in Legacy.
// validation should be in sync with one in New platform.
// https://github.com/elastic/kibana/blob/master/src/core/server/http/http_config.ts
basePath: Joi.string()
.default('')
.allow('')
.regex(/(^$|^\/.*[^\/]$)/, `start with a slash, don't end with one`),
host: Joi.string().hostname().default('localhost'),
port: Joi.number().default(5601),
rewriteBasePath: Joi.boolean().when('basePath', {
is: '',
then: Joi.default(false).valid(false),
otherwise: Joi.default(false),
}),
autoListen: HANDLED_IN_NEW_PLATFORM,
cors: HANDLED_IN_NEW_PLATFORM,
customResponseHeaders: HANDLED_IN_NEW_PLATFORM,
keepaliveTimeout: HANDLED_IN_NEW_PLATFORM,
maxPayloadBytes: HANDLED_IN_NEW_PLATFORM,
publicBaseUrl: HANDLED_IN_NEW_PLATFORM,
socketTimeout: HANDLED_IN_NEW_PLATFORM,
ssl: HANDLED_IN_NEW_PLATFORM,
compression: HANDLED_IN_NEW_PLATFORM,
uuid: HANDLED_IN_NEW_PLATFORM,
xsrf: HANDLED_IN_NEW_PLATFORM,
}).default(),
uiSettings: HANDLED_IN_NEW_PLATFORM,
logging: legacyLoggingConfigSchema,
ops: Joi.object({
interval: Joi.number().default(5000),
cGroupOverrides: HANDLED_IN_NEW_PLATFORM,
}).default(),
plugins: HANDLED_IN_NEW_PLATFORM,
path: HANDLED_IN_NEW_PLATFORM,
stats: HANDLED_IN_NEW_PLATFORM,
status: HANDLED_IN_NEW_PLATFORM,
map: HANDLED_IN_NEW_PLATFORM,
i18n: HANDLED_IN_NEW_PLATFORM,
// temporarily moved here from the (now deleted) kibana legacy plugin
kibana: Joi.object({
enabled: Joi.boolean().default(true),
index: Joi.string().default('.kibana'),
autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000),
// TODO Also allow units here like in elasticsearch config once this is moved to the new platform
autocompleteTimeout: Joi.number().integer().min(1).default(1000),
}).default(),
savedObjects: HANDLED_IN_NEW_PLATFORM,
}).default();

View file

@ -1,92 +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 schemaProvider from './schema';
import Joi from 'joi';
describe('Config schema', function () {
let schema;
beforeEach(async () => (schema = await schemaProvider()));
function validate(data, options) {
return Joi.validate(data, schema, options);
}
describe('server', function () {
it('everything is optional', function () {
const { error } = validate({});
expect(error).toBe(null);
});
describe('basePath', function () {
it('accepts empty strings', function () {
const { error, value } = validate({ server: { basePath: '' } });
expect(error).toBe(null);
expect(value.server.basePath).toBe('');
});
it('accepts strings with leading slashes', function () {
const { error, value } = validate({ server: { basePath: '/path' } });
expect(error).toBe(null);
expect(value.server.basePath).toBe('/path');
});
it('rejects strings with trailing slashes', function () {
const { error } = validate({ server: { basePath: '/path/' } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']);
});
it('rejects strings without leading slashes', function () {
const { error } = validate({ server: { basePath: 'path' } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']);
});
it('rejects things that are not strings', function () {
for (const value of [1, true, {}, [], /foo/]) {
const { error } = validate({ server: { basePath: value } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']);
}
});
});
describe('rewriteBasePath', function () {
it('defaults to false', () => {
const { error, value } = validate({});
expect(error).toBe(null);
expect(value.server.rewriteBasePath).toBe(false);
});
it('accepts false', function () {
const { error, value } = validate({ server: { rewriteBasePath: false } });
expect(error).toBe(null);
expect(value.server.rewriteBasePath).toBe(false);
});
it('accepts true if basePath set', function () {
const { error, value } = validate({ server: { basePath: '/foo', rewriteBasePath: true } });
expect(error).toBe(null);
expect(value.server.rewriteBasePath).toBe(true);
});
it('rejects true if basePath not set', function () {
const { error } = validate({ server: { rewriteBasePath: true } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']);
});
it('rejects strings', function () {
const { error } = validate({ server: { rewriteBasePath: 'foo' } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']);
});
});
});
});

View file

@ -1,20 +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 KbnServer from '../kbn_server';
/**
* Exposes `kbnServer.newPlatform` through Hapi API.
* @param kbnServer KbnServer singleton instance.
* @param server Hapi server instance to expose `core` on.
*/
export function coreMixin(kbnServer: KbnServer, server: Server) {
// we suppress type error because hapi expect a function here not an object
server.decorate('server', 'newPlatform', kbnServer.newPlatform as any);
}

View file

@ -1,37 +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 { format } from 'url';
import Boom from '@hapi/boom';
export default async function (kbnServer, server) {
server = kbnServer.server;
const getBasePath = (request) => kbnServer.newPlatform.setup.core.http.basePath.get(request);
server.route({
method: 'GET',
path: '/{p*}',
handler: function (req, h) {
const path = req.path;
if (path === '/' || path.charAt(path.length - 1) !== '/') {
throw Boom.notFound();
}
const basePath = getBasePath(req);
const pathPrefix = basePath ? `${basePath}/` : '';
return h
.redirect(
format({
search: req.url.search,
pathname: pathPrefix + path.slice(0, -1),
})
)
.permanent(true);
},
});
}

View file

@ -1,13 +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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/src/legacy/server'],
};

View file

@ -1,95 +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 {
CoreSetup,
CoreStart,
EnvironmentMode,
LoggerFactory,
PackageInfo,
LegacyServiceSetupDeps,
} from '../../core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LegacyConfig } from '../../core/server/legacy';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { UiPlugins } from '../../core/server/plugins';
// lot of legacy code was assuming this type only had these two methods
export type KibanaConfig = Pick<LegacyConfig, 'get' | 'has'>;
// Extend the defaults with the plugins and server methods we need.
declare module 'hapi' {
interface PluginProperties {
spaces: any;
}
interface Server {
config: () => KibanaConfig;
newPlatform: KbnServer['newPlatform'];
}
}
type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise<any> | void;
export interface PluginsSetup {
[key: string]: object;
}
export interface KibanaCore {
__internals: {
hapiServer: LegacyServiceSetupDeps['core']['http']['server'];
rendering: LegacyServiceSetupDeps['core']['rendering'];
uiPlugins: UiPlugins;
};
env: {
mode: Readonly<EnvironmentMode>;
packageInfo: Readonly<PackageInfo>;
};
setupDeps: {
core: CoreSetup;
plugins: PluginsSetup;
};
startDeps: {
core: CoreStart;
plugins: Record<string, object>;
};
logger: LoggerFactory;
}
export interface NewPlatform {
__internals: KibanaCore['__internals'];
env: KibanaCore['env'];
coreContext: {
logger: KibanaCore['logger'];
};
setup: KibanaCore['setupDeps'];
start: KibanaCore['startDeps'];
stop: null;
}
// eslint-disable-next-line import/no-default-export
export default class KbnServer {
public readonly newPlatform: NewPlatform;
public server: Server;
public inject: Server['inject'];
constructor(settings: Record<string, any>, config: KibanaConfig, core: KibanaCore);
public ready(): Promise<void>;
public mixin(...fns: KbnMixinFunc[]): Promise<void>;
public listen(): Promise<Server>;
public close(): Promise<void>;
public applyLoggingConfiguration(settings: any): void;
public config: KibanaConfig;
}
// Re-export commonly used hapi types.
export { Server, Request, ResponseToolkit } from '@hapi/hapi';

View file

@ -1,131 +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 { constant, once, compact, flatten } from 'lodash';
import { reconfigureLogging } from '@kbn/legacy-logging';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { fromRoot, pkg } from '../../core/server/utils';
import { Config } from './config';
import httpMixin from './http';
import { coreMixin } from './core';
import { loggingMixin } from './logging';
/**
* @typedef {import('./kbn_server').KibanaConfig} KibanaConfig
* @typedef {import('./kbn_server').KibanaCore} KibanaCore
* @typedef {import('./kbn_server').LegacyPlugins} LegacyPlugins
*/
const rootDir = fromRoot('.');
export default class KbnServer {
/**
* @param {Record<string, any>} settings
* @param {KibanaConfig} config
* @param {KibanaCore} core
*/
constructor(settings, config, core) {
this.name = pkg.name;
this.version = pkg.version;
this.build = pkg.build || false;
this.rootDir = rootDir;
this.settings = settings || {};
this.config = config;
const { setupDeps, startDeps, logger, __internals, env } = core;
this.server = __internals.hapiServer;
this.newPlatform = {
env: {
mode: env.mode,
packageInfo: env.packageInfo,
},
__internals,
coreContext: {
logger,
},
setup: setupDeps,
start: startDeps,
stop: null,
};
this.ready = constant(
this.mixin(
// Sets global HTTP behaviors
httpMixin,
coreMixin,
loggingMixin
)
);
this.listen = once(this.listen);
}
/**
* Extend the KbnServer outside of the constraints of a plugin. This allows access
* to APIs that are not exposed (intentionally) to the plugins and should only
* be used when the code will be kept up to date with Kibana.
*
* @param {...function} - functions that should be called to mixin functionality.
* They are called with the arguments (kibana, server, config)
* and can return a promise to delay execution of the next mixin
* @return {Promise} - promise that is resolved when the final mixin completes.
*/
async mixin(...fns) {
for (const fn of compact(flatten(fns))) {
await fn.call(this, this, this.server, this.config);
}
}
/**
* Tell the server to listen for incoming requests, or get
* a promise that will be resolved once the server is listening.
*
* @return undefined
*/
async listen() {
await this.ready();
const { server } = this;
if (process.env.isDevCliChild) {
// help parent process know when we are ready
process.send(['SERVER_LISTENING']);
}
return server;
}
async close() {
if (!this.server) {
return;
}
await this.server.stop();
}
async inject(opts) {
if (!this.server) {
await this.ready();
}
return await this.server.inject(opts);
}
applyLoggingConfiguration(settings) {
const config = Config.withDefaultSchema(settings);
const loggingConfig = config.get('logging');
const opsConfig = config.get('ops');
reconfigureLogging(this.server, loggingConfig, opsConfig.interval);
}
}

View file

@ -1,17 +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 { setupLogging, setupLoggingRotate } from '@kbn/legacy-logging';
export async function loggingMixin(kbnServer, server, config) {
const loggingConfig = config.get('logging');
const opsInterval = config.get('ops.interval');
await setupLogging(server, loggingConfig, opsInterval);
await setupLoggingRotate(server, loggingConfig);
}

View file

@ -1,11 +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 { pkg } from '../../core/server/utils';
export const IS_KIBANA_DISTRIBUTABLE = pkg.build && pkg.build.distributable === true;
export const IS_KIBANA_RELEASE = pkg.build && pkg.build.release === true;

View file

@ -1,68 +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 { deepCloneWithBuffers } from './deep_clone_with_buffers';
describe('deepCloneWithBuffers()', () => {
it('deep clones objects', () => {
const source = {
a: {
b: {},
c: {},
d: [
{
e: 'f',
},
],
},
};
const output = deepCloneWithBuffers(source);
expect(source.a).toEqual(output.a);
expect(source.a).not.toBe(output.a);
expect(source.a.b).toEqual(output.a.b);
expect(source.a.b).not.toBe(output.a.b);
expect(source.a.c).toEqual(output.a.c);
expect(source.a.c).not.toBe(output.a.c);
expect(source.a.d).toEqual(output.a.d);
expect(source.a.d).not.toBe(output.a.d);
expect(source.a.d[0]).toEqual(output.a.d[0]);
expect(source.a.d[0]).not.toBe(output.a.d[0]);
});
it('copies buffers but keeps them buffers', () => {
const input = Buffer.from('i am a teapot', 'utf8');
const output = deepCloneWithBuffers(input);
expect(Buffer.isBuffer(input)).toBe(true);
expect(Buffer.isBuffer(output)).toBe(true);
expect(Buffer.compare(output, input));
expect(output).not.toBe(input);
});
it('copies buffers that are deep', () => {
const input = {
a: {
b: {
c: Buffer.from('i am a teapot', 'utf8'),
},
},
};
const output = deepCloneWithBuffers(input);
expect(Buffer.isBuffer(input.a.b.c)).toBe(true);
expect(Buffer.isBuffer(output.a.b.c)).toBe(true);
expect(Buffer.compare(output.a.b.c, input.a.b.c));
expect(output.a.b.c).not.toBe(input.a.b.c);
});
});

View file

@ -1,22 +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 { cloneDeepWith } from 'lodash';
// We should add `any` return type to overcome bug in lodash types, customizer
// in lodash 3.* can return `undefined` if cloning is handled by the lodash, but
// type of the customizer function doesn't expect that.
function cloneBuffersCustomizer(val: unknown): any {
if (Buffer.isBuffer(val)) {
return Buffer.from(val);
}
}
export function deepCloneWithBuffers<T>(val: T): T {
return cloneDeepWith(val, cloneBuffersCustomizer);
}

Some files were not shown because too many files have changed in this diff Show more