Implements config deprecation in New Platform (#52251) (#52976)

* implements 'rename' and 'unset' deprecations

* introduce usage of ConfigDeprecationProvider

* adapt RawConfigService to only returns unmodified raw config

* apply deprecations when accessing config

* register legacy plugin deprecation in new platform

* implements ConfigService#validate

* add exemple config deprecation usage in testbed

* documentation

* export public config deprecation types

* fix new test due to rebase

* name ConfigDeprecationFactory

* update generated doc

* add tests for unset and move it to src/core/utils

* add tests for renameFromRoot and unusedFromRoot

* cast paths as any as get expects a fixed-length string array

* use specific logger for deprecations

* add additional test on renameFromRoot

* update migration guide

* migrate core deprecations to NP

* add integration test

* use same log context as legacy

* remove old deprecation warnings integration tests, now covered in NP

* migrates csp deprecation to NP

* removes deprecationWarningMixin from legacy

* remove legacy core deprecations

* remove unused import

* rename setupConfigSchemas to setupCoreConfig

* update generated doc
This commit is contained in:
Pierre Gayvallet 2019-12-13 11:44:39 +01:00 committed by GitHub
parent ad5b013d73
commit 09e269508a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 2399 additions and 723 deletions

View file

@ -9,5 +9,5 @@ Search for objects
<b>Signature:</b>
```typescript
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>>;
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>>;
```

View file

@ -20,7 +20,7 @@ export declare class SavedObjectsClient
| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | <code>(objects?: {</code><br/><code> id: string;</code><br/><code> type: string;</code><br/><code> }[]) =&gt; Promise&lt;SavedObjectsBatchResponse&lt;SavedObjectAttributes&gt;&gt;</code> | Returns an array of objects by id |
| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(type: string, attributes: T, options?: SavedObjectsCreateOptions) =&gt; Promise&lt;SimpleSavedObject&lt;T&gt;&gt;</code> | Persists an object |
| [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | <code>(type: string, id: string) =&gt; Promise&lt;{}&gt;</code> | Deletes an object |
| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(options: Pick&lt;SavedObjectFindOptionsServer, &quot;search&quot; &#124; &quot;filter&quot; &#124; &quot;type&quot; &#124; &quot;page&quot; &#124; &quot;fields&quot; &#124; &quot;searchFields&quot; &#124; &quot;defaultSearchOperator&quot; &#124; &quot;hasReference&quot; &#124; &quot;sortField&quot; &#124; &quot;perPage&quot;&gt;) =&gt; Promise&lt;SavedObjectsFindResponsePublic&lt;T&gt;&gt;</code> | Search for objects |
| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(options: Pick&lt;SavedObjectFindOptionsServer, &quot;search&quot; &#124; &quot;filter&quot; &#124; &quot;type&quot; &#124; &quot;page&quot; &#124; &quot;perPage&quot; &#124; &quot;sortField&quot; &#124; &quot;fields&quot; &#124; &quot;searchFields&quot; &#124; &quot;hasReference&quot; &#124; &quot;defaultSearchOperator&quot;&gt;) =&gt; Promise&lt;SavedObjectsFindResponsePublic&lt;T&gt;&gt;</code> | Search for objects |
| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(type: string, id: string) =&gt; Promise&lt;SimpleSavedObject&lt;T&gt;&gt;</code> | Fetches a single object |
## Methods

View file

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md)
## ConfigDeprecation type
Configuration deprecation returned from [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) that handles a single deprecation from the configuration.
<b>Signature:</b>
```typescript
export declare type ConfigDeprecation = (config: Record<string, any>, fromPath: string, logger: ConfigDeprecationLogger) => Record<string, any>;
```
## Remarks
This should only be manually implemented if [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) does not provide the proper helpers for a specific deprecation need.

View file

@ -0,0 +1,36 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md)
## ConfigDeprecationFactory interface
Provides helpers to generates the most commonly used [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) when invoking a [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md)<!-- -->.
See methods documentation for more detailed examples.
<b>Signature:</b>
```typescript
export interface ConfigDeprecationFactory
```
## Methods
| Method | Description |
| --- | --- |
| [rename(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.rename.md) | Rename a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the oldKey was found and deprecation applied. |
| [renameFromRoot(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) | Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied.<!-- -->This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead. |
| [unused(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unused.md) | Remove a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the unused key was found and deprecation applied. |
| [unusedFromRoot(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unusedfromroot.md) | Remove a configuration property from the root configuration. Will log a deprecation warning if the unused key was found and deprecation applied.<!-- -->This should be only used when removing properties from outside of a plugin's configuration. To remove properties from inside a plugin's configuration, use 'unused' instead. |
## Example
```typescript
const provider: ConfigDeprecationProvider = ({ rename, unused }) => [
rename('oldKey', 'newKey'),
unused('deprecatedKey'),
]
```

View file

@ -0,0 +1,36 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) &gt; [rename](./kibana-plugin-server.configdeprecationfactory.rename.md)
## ConfigDeprecationFactory.rename() method
Rename a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the oldKey was found and deprecation applied.
<b>Signature:</b>
```typescript
rename(oldKey: string, newKey: string): ConfigDeprecation;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| oldKey | <code>string</code> | |
| newKey | <code>string</code> | |
<b>Returns:</b>
`ConfigDeprecation`
## Example
Rename 'myplugin.oldKey' to 'myplugin.newKey'
```typescript
const provider: ConfigDeprecationProvider = ({ rename }) => [
rename('oldKey', 'newKey'),
]
```

View file

@ -0,0 +1,38 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) &gt; [renameFromRoot](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md)
## ConfigDeprecationFactory.renameFromRoot() method
Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied.
This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead.
<b>Signature:</b>
```typescript
renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| oldKey | <code>string</code> | |
| newKey | <code>string</code> | |
<b>Returns:</b>
`ConfigDeprecation`
## Example
Rename 'oldplugin.key' to 'newplugin.key'
```typescript
const provider: ConfigDeprecationProvider = ({ renameFromRoot }) => [
renameFromRoot('oldplugin.key', 'newplugin.key'),
]
```

View file

@ -0,0 +1,35 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) &gt; [unused](./kibana-plugin-server.configdeprecationfactory.unused.md)
## ConfigDeprecationFactory.unused() method
Remove a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the unused key was found and deprecation applied.
<b>Signature:</b>
```typescript
unused(unusedKey: string): ConfigDeprecation;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| unusedKey | <code>string</code> | |
<b>Returns:</b>
`ConfigDeprecation`
## Example
Flags 'myplugin.deprecatedKey' as unused
```typescript
const provider: ConfigDeprecationProvider = ({ unused }) => [
unused('deprecatedKey'),
]
```

View file

@ -0,0 +1,37 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) &gt; [unusedFromRoot](./kibana-plugin-server.configdeprecationfactory.unusedfromroot.md)
## ConfigDeprecationFactory.unusedFromRoot() method
Remove a configuration property from the root configuration. Will log a deprecation warning if the unused key was found and deprecation applied.
This should be only used when removing properties from outside of a plugin's configuration. To remove properties from inside a plugin's configuration, use 'unused' instead.
<b>Signature:</b>
```typescript
unusedFromRoot(unusedKey: string): ConfigDeprecation;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| unusedKey | <code>string</code> | |
<b>Returns:</b>
`ConfigDeprecation`
## Example
Flags 'somepath.deprecatedProperty' as unused
```typescript
const provider: ConfigDeprecationProvider = ({ unusedFromRoot }) => [
unusedFromRoot('somepath.deprecatedProperty'),
]
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ConfigDeprecationLogger](./kibana-plugin-server.configdeprecationlogger.md)
## ConfigDeprecationLogger type
Logger interface used when invoking a [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md)
<b>Signature:</b>
```typescript
export declare type ConfigDeprecationLogger = (message: string) => void;
```

View file

@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md)
## ConfigDeprecationProvider type
A provider that should returns a list of [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md)<!-- -->.
See [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) for more usage examples.
<b>Signature:</b>
```typescript
export declare type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[];
```
## Example
```typescript
const provider: ConfigDeprecationProvider = ({ rename, unused }) => [
rename('oldKey', 'newKey'),
unused('deprecatedKey'),
myCustomDeprecation,
]
```

View file

@ -46,6 +46,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [Capabilities](./kibana-plugin-server.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
| [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | APIs to manage the [Capabilities](./kibana-plugin-server.capabilities.md) that will be used by the application.<!-- -->Plugins relying on capabilities to toggle some of their features should register them during the setup phase using the <code>registerProvider</code> method.<!-- -->Plugins having the responsibility to restrict capabilities depending on a given context should register their capabilities switcher using the <code>registerSwitcher</code> method.<!-- -->Refers to the methods documentation for complete description and examples. |
| [CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md) | APIs to access the application [Capabilities](./kibana-plugin-server.capabilities.md)<!-- -->. |
| [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) | Provides helpers to generates the most commonly used [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) when invoking a [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md)<!-- -->.<!-- -->See methods documentation for more detailed examples. |
| [ContextSetup](./kibana-plugin-server.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. |
| [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins <code>setup</code> method. |
| [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins <code>start</code> method. |
@ -82,7 +83,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. |
| [PackageInfo](./kibana-plugin-server.packageinfo.md) | |
| [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration schema and capabilities. |
| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. |
| [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. |
| [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. |
| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | |
@ -152,6 +153,9 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [AuthResult](./kibana-plugin-server.authresult.md) | |
| [CapabilitiesProvider](./kibana-plugin-server.capabilitiesprovider.md) | See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) |
| [CapabilitiesSwitcher](./kibana-plugin-server.capabilitiesswitcher.md) | See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) |
| [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) | Configuration deprecation returned from [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) that handles a single deprecation from the configuration. |
| [ConfigDeprecationLogger](./kibana-plugin-server.configdeprecationlogger.md) | Logger interface used when invoking a [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) |
| [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) | A provider that should returns a list of [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md)<!-- -->.<!-- -->See [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) for more usage examples. |
| [ConfigPath](./kibana-plugin-server.configpath.md) | |
| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | |
| [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) &gt; [deprecations](./kibana-plugin-server.pluginconfigdescriptor.deprecations.md)
## PluginConfigDescriptor.deprecations property
Provider for the [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) to apply to the plugin configuration.
<b>Signature:</b>
```typescript
deprecations?: ConfigDeprecationProvider;
```

View file

@ -4,7 +4,7 @@
## PluginConfigDescriptor interface
Describes a plugin configuration schema and capabilities.
Describes a plugin configuration properties.
<b>Signature:</b>
@ -16,6 +16,7 @@ export interface PluginConfigDescriptor<T = any>
| Property | Type | Description |
| --- | --- | --- |
| [deprecations](./kibana-plugin-server.pluginconfigdescriptor.deprecations.md) | <code>ConfigDeprecationProvider</code> | Provider for the [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) to apply to the plugin configuration. |
| [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | <code>{</code><br/><code> [P in keyof T]?: boolean;</code><br/><code> }</code> | List of configuration properties that will be available on the client-side plugin. |
| [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | <code>PluginConfigSchema&lt;T&gt;</code> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) |
@ -39,6 +40,10 @@ export const config: PluginConfigDescriptor<ConfigType> = {
uiProp: true,
},
schema: configSchema,
deprecations: ({ rename, unused }) => [
rename('securityKey', 'secret'),
unused('deprecatedProperty'),
],
};
```

View file

@ -29,7 +29,6 @@ import { REPO_ROOT } from '@kbn/dev-utils';
import Log from '../log';
import Worker from './worker';
import { Config } from '../../legacy/server/config/config';
import { transformDeprecations } from '../../legacy/server/config/transform_deprecations';
process.env.kbnWorkerType = 'managr';
@ -37,7 +36,7 @@ export default class ClusterManager {
static create(opts, settings = {}, basePathProxy) {
return new ClusterManager(
opts,
Config.withDefaultSchema(transformDeprecations(settings)),
Config.withDefaultSchema(settings),
basePathProxy
);
}

View file

@ -47,6 +47,7 @@
- [UI Exports](#ui-exports)
- [How to](#how-to)
- [Configure plugin](#configure-plugin)
- [Handle plugin configuration deprecations](#handle-plugin-config-deprecations)
- [Mock new platform services in tests](#mock-new-platform-services-in-tests)
- [Writing mocks for your plugin](#writing-mocks-for-your-plugin)
- [Using mocks in your tests](#using-mocks-in-your-tests)
@ -1220,13 +1221,20 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy';
In server code, `core` can be accessed from either `server.newPlatform` or `kbnServer.newPlatform`. There are not currently very many services available on the server-side:
| Legacy Platform | New Platform | Notes |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ |
| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) |
| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | |
| `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client |
| `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client |
| Legacy Platform | New Platform | Notes |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ |
| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) |
| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | |
| `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client |
| `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client |
| `xpackMainPlugin.info.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | |
| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactory`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) | |
| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | |
| `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) | |
| `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md) | |
| `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md) | |
| `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration |
| `xpackMainPlugin.info.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | |
_See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-server.coresetup.md)_
@ -1399,6 +1407,52 @@ export const config = {
};
```
#### Handle plugin configuration deprecations
If your plugin have deprecated properties, you can describe them using the `deprecations` config descriptor field.
The system is quite similar to the legacy plugin's deprecation management. The most important difference
is that deprecations are managed on a per-plugin basis, meaning that you don't need to specify the whole
property path, but use the relative path from your plugin's configuration root.
```typescript
// my_plugin/server/index.ts
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginConfigDescriptor } from 'kibana/server';
const configSchema = schema.object({
newProperty: schema.string({ defaultValue: 'Some string' }),
});
type ConfigType = TypeOf<typeof configSchema>;
export const config: PluginConfigDescriptor<ConfigType> = {
schema: configSchema,
deprecations: ({ rename, unused }) => [
rename('oldProperty', 'newProperty'),
unused('someUnusedProperty'),
]
};
```
In some cases, accessing the whole configuration for deprecations is necessary. For these edge cases,
`renameFromRoot` and `unusedFromRoot` are also accessible when declaring deprecations.
```typescript
// my_plugin/server/index.ts
export const config: PluginConfigDescriptor<ConfigType> = {
schema: configSchema,
deprecations: ({ renameFromRoot, unusedFromRoot }) => [
renameFromRoot('oldplugin.property', 'myplugin.property'),
unusedFromRoot('oldplugin.deprecated'),
]
};
```
Note that deprecations registered in new platform's plugins are not applied to the legacy configuration.
During migration, if you still need the deprecations to be effective in the legacy plugin, you need to declare them in
both plugin definitions.
### Mock new platform services in tests
#### Writing mocks for your plugin

View file

@ -886,7 +886,7 @@ export class SavedObjectsClient {
bulkUpdate<T extends SavedObjectAttributes>(objects?: SavedObjectsBulkUpdateObject[]): Promise<SavedObjectsBatchResponse<SavedObjectAttributes>>;
create: <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>>;
delete: (type: string, id: string) => Promise<{}>;
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectsFindOptions, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>>;
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectsFindOptions, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>>;
get: <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>>;
update<T extends SavedObjectAttributes>(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise<SimpleSavedObject<T>>;
}

View file

@ -20,7 +20,6 @@
import chalk from 'chalk';
import { isMaster } from 'cluster';
import { CliArgs, Env, RawConfigService } from './config';
import { LegacyObjectToConfigAdapter } from './legacy';
import { Root } from './root';
import { CriticalError } from './errors';
@ -62,14 +61,10 @@ export async function bootstrap({
isDevClusterMaster: isMaster && cliArgs.dev && features.isClusterModeSupported,
});
const rawConfigService = new RawConfigService(
env.configs,
rawConfig => new LegacyObjectToConfigAdapter(applyConfigOverrides(rawConfig))
);
const rawConfigService = new RawConfigService(env.configs, applyConfigOverrides);
rawConfigService.loadConfig();
const root = new Root(rawConfigService.getConfig$(), env, onRootShutdown);
const root = new Root(rawConfigService, env, onRootShutdown);
process.on('SIGHUP', () => reloadLoggingConfig());

View file

@ -34,6 +34,8 @@ const createConfigServiceMock = ({
getUnusedPaths: jest.fn(),
isEnabledAtPath: jest.fn(),
setSchema: jest.fn(),
addDeprecationProvider: jest.fn(),
validate: jest.fn(),
};
mocked.atPath.mockReturnValue(new BehaviorSubject(atPath));
mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$)));

View file

@ -19,3 +19,8 @@
export const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] });
jest.mock('../../../../package.json', () => mockPackage);
export const mockApplyDeprecations = jest.fn((config, deprecations, log) => config);
jest.mock('./deprecation/apply_deprecations', () => ({
applyDeprecations: mockApplyDeprecations,
}));

View file

@ -20,13 +20,14 @@
/* eslint-disable max-classes-per-file */
import { BehaviorSubject, Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { first, take } from 'rxjs/operators';
import { mockPackage } from './config_service.test.mocks';
import { mockPackage, mockApplyDeprecations } from './config_service.test.mocks';
import { rawConfigServiceMock } from './raw_config_service.mock';
import { schema } from '@kbn/config-schema';
import { ConfigService, Env, ObjectToConfigAdapter } from '.';
import { ConfigService, Env } from '.';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { getEnvOptions } from './__mocks__/env';
@ -34,9 +35,12 @@ const emptyArgv = getEnvOptions();
const defaultEnv = new Env('/kibana', emptyArgv);
const logger = loggingServiceMock.create();
const getRawConfigProvider = (rawConfig: Record<string, any>) =>
rawConfigServiceMock.create({ rawConfig });
test('returns config at path as observable', async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'foo' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfig = getRawConfigProvider({ key: 'foo' });
const configService = new ConfigService(rawConfig, defaultEnv, logger);
const stringSchema = schema.string();
await configService.setSchema('key', stringSchema);
@ -48,21 +52,36 @@ test('returns config at path as observable', async () => {
});
test('throws if config at path does not match schema', async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 123 }));
const rawConfig = getRawConfigProvider({ key: 123 });
const configService = new ConfigService(config$, defaultEnv, logger);
const configService = new ConfigService(rawConfig, defaultEnv, logger);
await configService.setSchema('key', schema.string());
await expect(
configService.setSchema('key', schema.string())
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[config validation of [key]]: expected value of type [string] but got [number]"`
);
const valuesReceived: any[] = [];
await configService
.atPath('key')
.pipe(take(1))
.subscribe(
value => {
valuesReceived.push(value);
},
error => {
valuesReceived.push(error);
}
);
await expect(valuesReceived).toMatchInlineSnapshot(`
Array [
[Error: [config validation of [key]]: expected value of type [string] but got [number]],
]
`);
});
test('re-validate config when updated', async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const rawConfig$ = new BehaviorSubject<Record<string, any>>({ key: 'value' });
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ });
const configService = new ConfigService(config$, defaultEnv, logger);
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
configService.setSchema('key', schema.string());
const valuesReceived: any[] = [];
@ -75,19 +94,19 @@ test('re-validate config when updated', async () => {
}
);
config$.next(new ObjectToConfigAdapter({ key: 123 }));
rawConfig$.next({ key: 123 });
await expect(valuesReceived).toMatchInlineSnapshot(`
Array [
"value",
[Error: [config validation of [key]]: expected value of type [string] but got [number]],
]
Array [
"value",
[Error: [config validation of [key]]: expected value of type [string] but got [number]],
]
`);
});
test("returns undefined if fetching optional config at a path that doesn't exist", async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({}));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfig = getRawConfigProvider({});
const configService = new ConfigService(rawConfig, defaultEnv, logger);
const value$ = configService.optionalAtPath('unique-name');
const value = await value$.pipe(first()).toPromise();
@ -96,8 +115,8 @@ test("returns undefined if fetching optional config at a path that doesn't exist
});
test('returns observable config at optional path if it exists', async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ value: 'bar' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfig = getRawConfigProvider({ value: 'bar' });
const configService = new ConfigService(rawConfig, defaultEnv, logger);
await configService.setSchema('value', schema.string());
const value$ = configService.optionalAtPath('value');
@ -107,8 +126,10 @@ test('returns observable config at optional path if it exists', async () => {
});
test("does not push new configs when reloading if config at path hasn't changed", async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfig$ = new BehaviorSubject<Record<string, any>>({ key: 'value' });
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
await configService.setSchema('key', schema.string());
const valuesReceived: any[] = [];
@ -116,14 +137,16 @@ test("does not push new configs when reloading if config at path hasn't changed"
valuesReceived.push(value);
});
config$.next(new ObjectToConfigAdapter({ key: 'value' }));
rawConfig$.next({ key: 'value' });
expect(valuesReceived).toEqual(['value']);
});
test('pushes new config when reloading and config at path has changed', async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfig$ = new BehaviorSubject<Record<string, any>>({ key: 'value' });
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
await configService.setSchema('key', schema.string());
const valuesReceived: any[] = [];
@ -131,14 +154,14 @@ test('pushes new config when reloading and config at path has changed', async ()
valuesReceived.push(value);
});
config$.next(new ObjectToConfigAdapter({ key: 'new value' }));
rawConfig$.next({ key: 'new value' });
expect(valuesReceived).toEqual(['value', 'new value']);
});
test("throws error if 'schema' is not defined for a key", async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: { key: 'value' } });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
const configs = configService.atPath('key');
@ -148,8 +171,8 @@ test("throws error if 'schema' is not defined for a key", async () => {
});
test("throws error if 'setSchema' called several times for the same key", async () => {
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' }));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: { key: 'value' } });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
const addSchema = async () => await configService.setSchema('key', schema.string());
await addSchema();
await expect(addSchema()).rejects.toMatchInlineSnapshot(
@ -157,6 +180,32 @@ test("throws error if 'setSchema' called several times for the same key", async
);
});
test('flags schema paths as handled when registering a schema', async () => {
const rawConfigProvider = rawConfigServiceMock.create({
rawConfig: {
service: {
string: 'str',
number: 42,
},
},
});
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
await configService.setSchema(
'service',
schema.object({
string: schema.string(),
number: schema.number(),
})
);
expect(await configService.getUsedPaths()).toMatchInlineSnapshot(`
Array [
"service.string",
"service.number",
]
`);
});
test('tracks unhandled paths', async () => {
const initialConfig = {
bar: {
@ -178,8 +227,8 @@ test('tracks unhandled paths', async () => {
},
};
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
configService.atPath('foo');
configService.atPath(['bar', 'deep2']);
@ -201,8 +250,8 @@ test('correctly passes context', async () => {
};
const env = new Env('/kibana', getEnvOptions());
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: { foo: {} } });
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ foo: {} }));
const schemaDefinition = schema.object({
branchRef: schema.string({
defaultValue: schema.contextRef('branch'),
@ -219,7 +268,7 @@ test('correctly passes context', async () => {
defaultValue: schema.contextRef('version'),
}),
});
const configService = new ConfigService(config$, env, logger);
const configService = new ConfigService(rawConfigProvider, env, logger);
await configService.setSchema('foo', schemaDefinition);
const value$ = configService.atPath('foo');
@ -234,8 +283,8 @@ test('handles enabled path, but only marks the enabled path as used', async () =
},
};
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
const isEnabled = await configService.isEnabledAtPath('pid');
expect(isEnabled).toBe(true);
@ -252,8 +301,8 @@ test('handles enabled path when path is array', async () => {
},
};
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
const isEnabled = await configService.isEnabledAtPath(['pid']);
expect(isEnabled).toBe(true);
@ -270,8 +319,8 @@ test('handles disabled path and marks config as used', async () => {
},
};
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
const isEnabled = await configService.isEnabledAtPath('pid');
expect(isEnabled).toBe(false);
@ -287,9 +336,9 @@ test('does not throw if schema does not define "enabled" schema', async () => {
},
};
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
expect(
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
await expect(
configService.setSchema(
'pid',
schema.object({
@ -310,8 +359,8 @@ test('does not throw if schema does not define "enabled" schema', async () => {
test('treats config as enabled if config path is not present in config', async () => {
const initialConfig = {};
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
const isEnabled = await configService.isEnabledAtPath('pid');
expect(isEnabled).toBe(true);
@ -327,8 +376,8 @@ test('read "enabled" even if its schema is not present', async () => {
},
};
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
const isEnabled = await configService.isEnabledAtPath('foo');
expect(isEnabled).toBe(true);
@ -337,8 +386,8 @@ test('read "enabled" even if its schema is not present', async () => {
test('allows plugins to specify "enabled" flag via validation schema', async () => {
const initialConfig = {};
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
const configService = new ConfigService(config$, defaultEnv, logger);
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
await configService.setSchema(
'foo',
@ -361,3 +410,49 @@ test('allows plugins to specify "enabled" flag via validation schema', async ()
expect(await configService.isEnabledAtPath('baz')).toBe(true);
});
test('does not throw during validation is every schema is valid', async () => {
const rawConfig = getRawConfigProvider({ stringKey: 'foo', numberKey: 42 });
const configService = new ConfigService(rawConfig, defaultEnv, logger);
await configService.setSchema('stringKey', schema.string());
await configService.setSchema('numberKey', schema.number());
await expect(configService.validate()).resolves.toBeUndefined();
});
test('throws during validation is any schema is invalid', async () => {
const rawConfig = getRawConfigProvider({ stringKey: 123, numberKey: 42 });
const configService = new ConfigService(rawConfig, defaultEnv, logger);
await configService.setSchema('stringKey', schema.string());
await configService.setSchema('numberKey', schema.number());
await expect(configService.validate()).rejects.toThrowErrorMatchingInlineSnapshot(
`"[config validation of [stringKey]]: expected value of type [string] but got [number]"`
);
});
test('logs deprecation warning during validation', async () => {
const rawConfig = getRawConfigProvider({});
const configService = new ConfigService(rawConfig, defaultEnv, logger);
mockApplyDeprecations.mockImplementationOnce((config, deprecations, log) => {
log('some deprecation message');
log('another deprecation message');
return config;
});
loggingServiceMock.clear(logger);
await configService.validate();
expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"some deprecation message",
],
Array [
"another deprecation message",
],
]
`);
});

View file

@ -19,12 +19,20 @@
import { Type } from '@kbn/config-schema';
import { isEqual } from 'lodash';
import { Observable } from 'rxjs';
import { distinctUntilChanged, first, map } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, first, map, shareReplay, take } from 'rxjs/operators';
import { Config, ConfigPath, Env } from '.';
import { Logger, LoggerFactory } from '../logging';
import { hasConfigPathIntersection } from './config';
import { RawConfigurationProvider } from './raw_config_service';
import {
applyDeprecations,
ConfigDeprecationWithContext,
ConfigDeprecationProvider,
configDeprecationFactory,
} from './deprecation';
import { LegacyObjectToConfigAdapter } from '../legacy/config';
/** @internal */
export type IConfigService = PublicMethodsOf<ConfigService>;
@ -32,6 +40,9 @@ export type IConfigService = PublicMethodsOf<ConfigService>;
/** @internal */
export class ConfigService {
private readonly log: Logger;
private readonly deprecationLog: Logger;
private readonly config$: Observable<Config>;
/**
* Whenever a config if read at a path, we mark that path as 'handled'. We can
@ -39,13 +50,23 @@ export class ConfigService {
*/
private readonly handledPaths: ConfigPath[] = [];
private readonly schemas = new Map<string, Type<unknown>>();
private readonly deprecations = new BehaviorSubject<ConfigDeprecationWithContext[]>([]);
constructor(
private readonly config$: Observable<Config>,
private readonly rawConfigProvider: RawConfigurationProvider,
private readonly env: Env,
logger: LoggerFactory
) {
this.log = logger.get('config');
this.deprecationLog = logger.get('config', 'deprecation');
this.config$ = combineLatest([this.rawConfigProvider.getConfig$(), this.deprecations]).pipe(
map(([rawConfig, deprecations]) => {
const migrated = applyDeprecations(rawConfig, deprecations);
return new LegacyObjectToConfigAdapter(migrated);
}),
shareReplay(1)
);
}
/**
@ -58,10 +79,37 @@ export class ConfigService {
}
this.schemas.set(namespace, schema);
this.markAsHandled(path);
}
await this.validateConfig(path)
.pipe(first())
.toPromise();
/**
* Register a {@link ConfigDeprecationProvider} to be used when validating and migrating the configuration
*/
public addDeprecationProvider(path: ConfigPath, provider: ConfigDeprecationProvider) {
const flatPath = pathToString(path);
this.deprecations.next([
...this.deprecations.value,
...provider(configDeprecationFactory).map(deprecation => ({
deprecation,
path: flatPath,
})),
]);
}
/**
* Validate the whole configuration and log the deprecation warnings.
*
* This must be done after every schemas and deprecation providers have been registered.
*/
public async validate() {
const namespaces = [...this.schemas.keys()];
for (let i = 0; i < namespaces.length; i++) {
await this.validateConfigAtPath(namespaces[i])
.pipe(first())
.toPromise();
}
await this.logDeprecation();
}
/**
@ -79,7 +127,7 @@ export class ConfigService {
* @param path - The path to the desired subset of the config.
*/
public atPath<TSchema>(path: ConfigPath) {
return this.validateConfig(path) as Observable<TSchema>;
return this.validateConfigAtPath(path) as Observable<TSchema>;
}
/**
@ -92,7 +140,7 @@ export class ConfigService {
return this.getDistinctConfig(path).pipe(
map(config => {
if (config === undefined) return undefined;
return this.validate(path, config) as TSchema;
return this.validateAtPath(path, config) as TSchema;
})
);
}
@ -148,7 +196,21 @@ export class ConfigService {
return config.getFlattenedPaths().filter(path => isPathHandled(path, handledPaths));
}
private validate(path: ConfigPath, config: Record<string, unknown>) {
private async logDeprecation() {
const rawConfig = await this.rawConfigProvider
.getConfig$()
.pipe(take(1))
.toPromise();
const deprecations = await this.deprecations.pipe(take(1)).toPromise();
const deprecationMessages: string[] = [];
const logger = (msg: string) => deprecationMessages.push(msg);
applyDeprecations(rawConfig, deprecations, logger);
deprecationMessages.forEach(msg => {
this.deprecationLog.warn(msg);
});
}
private validateAtPath(path: ConfigPath, config: Record<string, unknown>) {
const namespace = pathToString(path);
const schema = this.schemas.get(namespace);
if (!schema) {
@ -165,8 +227,8 @@ export class ConfigService {
);
}
private validateConfig(path: ConfigPath) {
return this.getDistinctConfig(path).pipe(map(config => this.validate(path, config)));
private validateConfigAtPath(path: ConfigPath) {
return this.getDistinctConfig(path).pipe(map(config => this.validateAtPath(path, config)));
}
private getDistinctConfig(path: ConfigPath) {

View file

@ -0,0 +1,85 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { applyDeprecations } from './apply_deprecations';
import { ConfigDeprecation, ConfigDeprecationWithContext } from './types';
import { configDeprecationFactory as deprecations } from './deprecation_factory';
const wrapHandler = (
handler: ConfigDeprecation,
path: string = ''
): ConfigDeprecationWithContext => ({
deprecation: handler,
path,
});
describe('applyDeprecations', () => {
it('calls all deprecations handlers once', () => {
const handlerA = jest.fn();
const handlerB = jest.fn();
const handlerC = jest.fn();
applyDeprecations(
{},
[handlerA, handlerB, handlerC].map(h => wrapHandler(h))
);
expect(handlerA).toHaveBeenCalledTimes(1);
expect(handlerB).toHaveBeenCalledTimes(1);
expect(handlerC).toHaveBeenCalledTimes(1);
});
it('calls handlers with correct arguments', () => {
const logger = () => undefined;
const initialConfig = { foo: 'bar', deprecated: 'deprecated' };
const alteredConfig = { foo: 'bar' };
const handlerA = jest.fn().mockReturnValue(alteredConfig);
const handlerB = jest.fn().mockImplementation(conf => conf);
applyDeprecations(
initialConfig,
[wrapHandler(handlerA, 'pathA'), wrapHandler(handlerB, 'pathB')],
logger
);
expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', logger);
expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', logger);
});
it('returns the migrated config', () => {
const initialConfig = { foo: 'bar', deprecated: 'deprecated', renamed: 'renamed' };
const migrated = applyDeprecations(initialConfig, [
wrapHandler(deprecations.unused('deprecated')),
wrapHandler(deprecations.rename('renamed', 'newname')),
]);
expect(migrated).toEqual({ foo: 'bar', newname: 'renamed' });
});
it('does not alter the initial config', () => {
const initialConfig = { foo: 'bar', deprecated: 'deprecated' };
const migrated = applyDeprecations(initialConfig, [
wrapHandler(deprecations.unused('deprecated')),
]);
expect(initialConfig).toEqual({ foo: 'bar', deprecated: 'deprecated' });
expect(migrated).toEqual({ foo: 'bar' });
});
});

View file

@ -0,0 +1,40 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { cloneDeep } from 'lodash';
import { ConfigDeprecationWithContext, ConfigDeprecationLogger } from './types';
const noopLogger = (msg: string) => undefined;
/**
* Applies deprecations on given configuration and logs any deprecation warning using provided logger.
*
* @internal
*/
export const applyDeprecations = (
config: Record<string, any>,
deprecations: ConfigDeprecationWithContext[],
logger: ConfigDeprecationLogger = noopLogger
) => {
let processed = cloneDeep(config);
deprecations.forEach(({ deprecation, path }) => {
processed = deprecation(processed, path, logger);
});
return processed;
};

View file

@ -0,0 +1,211 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { coreDeprecationProvider } from './core_deprecations';
import { configDeprecationFactory } from './deprecation_factory';
import { applyDeprecations } from './apply_deprecations';
const initialEnv = { ...process.env };
const applyCoreDeprecations = (settings: Record<string, any> = {}) => {
const deprecations = coreDeprecationProvider(configDeprecationFactory);
const deprecationMessages: string[] = [];
const migrated = applyDeprecations(
settings,
deprecations.map(deprecation => ({
deprecation,
path: '',
})),
msg => deprecationMessages.push(msg)
);
return {
messages: deprecationMessages,
migrated,
};
};
describe('core deprecations', () => {
beforeEach(() => {
process.env = { ...initialEnv };
});
describe('configPath', () => {
it('logs a warning if CONFIG_PATH environ variable is set', () => {
process.env.CONFIG_PATH = 'somepath';
const { messages } = applyCoreDeprecations();
expect(messages).toMatchInlineSnapshot(`
Array [
"Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder",
]
`);
});
it('does not log a warning if CONFIG_PATH environ variable is unset', () => {
delete process.env.CONFIG_PATH;
const { messages } = applyCoreDeprecations();
expect(messages).toHaveLength(0);
});
});
describe('dataPath', () => {
it('logs a warning if DATA_PATH environ variable is set', () => {
process.env.DATA_PATH = 'somepath';
const { messages } = applyCoreDeprecations();
expect(messages).toMatchInlineSnapshot(`
Array [
"Environment variable \\"DATA_PATH\\" will be removed. It has been replaced with kibana.yml setting \\"path.data\\"",
]
`);
});
it('does not log a warning if DATA_PATH environ variable is unset', () => {
delete process.env.DATA_PATH;
const { messages } = applyCoreDeprecations();
expect(messages).toHaveLength(0);
});
});
describe('rewriteBasePath', () => {
it('logs a warning is server.basePath is set and server.rewriteBasePath is not', () => {
const { messages } = applyCoreDeprecations({
server: {
basePath: 'foo',
},
});
expect(messages).toMatchInlineSnapshot(`
Array [
"You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana will expect that all requests start with server.basePath rather than expecting you to rewrite the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the current behavior and silence this warning.",
]
`);
});
it('does not log a warning if both server.basePath and server.rewriteBasePath are unset', () => {
const { messages } = applyCoreDeprecations({
server: {},
});
expect(messages).toHaveLength(0);
});
it('does not log a warning if both server.basePath and server.rewriteBasePath are set', () => {
const { messages } = applyCoreDeprecations({
server: {
basePath: 'foo',
rewriteBasePath: true,
},
});
expect(messages).toHaveLength(0);
});
});
describe('cspRulesDeprecation', () => {
describe('with nonce source', () => {
it('logs a warning', () => {
const settings = {
csp: {
rules: [`script-src 'self' 'nonce-{nonce}'`],
},
};
const { messages } = applyCoreDeprecations(settings);
expect(messages).toMatchInlineSnapshot(`
Array [
"csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src",
]
`);
});
it('replaces a nonce', () => {
expect(
applyCoreDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}'`] } }).migrated.csp
.rules
).toEqual([`script-src 'self'`]);
expect(
applyCoreDeprecations({ csp: { rules: [`script-src 'unsafe-eval' 'nonce-{nonce}'`] } })
.migrated.csp.rules
).toEqual([`script-src 'unsafe-eval' 'self'`]);
});
it('removes a quoted nonce', () => {
expect(
applyCoreDeprecations({ csp: { rules: [`script-src 'self' 'nonce-{nonce}'`] } }).migrated
.csp.rules
).toEqual([`script-src 'self'`]);
expect(
applyCoreDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}' 'self'`] } }).migrated
.csp.rules
).toEqual([`script-src 'self'`]);
});
it('removes a non-quoted nonce', () => {
expect(
applyCoreDeprecations({ csp: { rules: [`script-src 'self' nonce-{nonce}`] } }).migrated
.csp.rules
).toEqual([`script-src 'self'`]);
expect(
applyCoreDeprecations({ csp: { rules: [`script-src nonce-{nonce} 'self'`] } }).migrated
.csp.rules
).toEqual([`script-src 'self'`]);
});
it('removes a strange nonce', () => {
expect(
applyCoreDeprecations({ csp: { rules: [`script-src 'self' blah-{nonce}-wow`] } }).migrated
.csp.rules
).toEqual([`script-src 'self'`]);
});
it('removes multiple nonces', () => {
expect(
applyCoreDeprecations({
csp: {
rules: [
`script-src 'nonce-{nonce}' 'self' blah-{nonce}-wow`,
`style-src 'nonce-{nonce}' 'self'`,
],
},
}).migrated.csp.rules
).toEqual([`script-src 'self'`, `style-src 'self'`]);
});
});
describe('without self source', () => {
it('logs a warning', () => {
const { messages } = applyCoreDeprecations({
csp: { rules: [`script-src 'unsafe-eval'`] },
});
expect(messages).toMatchInlineSnapshot(`
Array [
"csp.rules must contain the 'self' source. Automatically adding to script-src.",
]
`);
});
it('adds self', () => {
expect(
applyCoreDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }).migrated.csp.rules
).toEqual([`script-src 'unsafe-eval' 'self'`]);
});
});
it('does not add self to other policies', () => {
expect(
applyCoreDeprecations({ csp: { rules: [`worker-src blob:`] } }).migrated.csp.rules
).toEqual([`worker-src blob:`]);
});
});
});

View file

@ -0,0 +1,114 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { has, get } from 'lodash';
import { ConfigDeprecationProvider, ConfigDeprecation } from './types';
const configPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
if (has(process.env, 'CONFIG_PATH')) {
log(
`Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder`
);
}
return settings;
};
const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
if (has(process.env, 'DATA_PATH')) {
log(
`Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"`
);
}
return settings;
};
const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) {
log(
'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' +
'will expect that all requests start with server.basePath rather than expecting you to rewrite ' +
'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' +
'current behavior and silence this warning.'
);
}
return settings;
};
const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
const NONCE_STRING = `{nonce}`;
// Policies that should include the 'self' source
const SELF_POLICIES = Object.freeze(['script-src', 'style-src']);
const SELF_STRING = `'self'`;
const rules: string[] = get(settings, 'csp.rules');
if (rules) {
const parsed = new Map(
rules.map(ruleStr => {
const parts = ruleStr.split(/\s+/);
return [parts[0], parts.slice(1)];
})
);
settings.csp.rules = [...parsed].map(([policy, sourceList]) => {
if (sourceList.find(source => source.includes(NONCE_STRING))) {
log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`);
sourceList = sourceList.filter(source => !source.includes(NONCE_STRING));
// Add 'self' if not present
if (!sourceList.find(source => source.includes(SELF_STRING))) {
sourceList.push(SELF_STRING);
}
}
if (
SELF_POLICIES.includes(policy) &&
!sourceList.find(source => source.includes(SELF_STRING))
) {
log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`);
sourceList.push(SELF_STRING);
}
return `${policy} ${sourceList.join(' ')}`.trim();
});
}
return settings;
};
export const coreDeprecationProvider: ConfigDeprecationProvider = ({
unusedFromRoot,
renameFromRoot,
}) => [
unusedFromRoot('savedObjects.indexCheckTimeout'),
unusedFromRoot('server.xsrf.token'),
unusedFromRoot('uiSettings.enabled'),
renameFromRoot('optimize.lazy', 'optimize.watch'),
renameFromRoot('optimize.lazyPort', 'optimize.watchPort'),
renameFromRoot('optimize.lazyHost', 'optimize.watchHost'),
renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'),
renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'),
renameFromRoot('xpack.telemetry.enabled', 'telemetry.enabled'),
renameFromRoot('xpack.telemetry.config', 'telemetry.config'),
renameFromRoot('xpack.telemetry.banner', 'telemetry.banner'),
renameFromRoot('xpack.telemetry.url', 'telemetry.url'),
configPathDeprecation,
dataPathDeprecation,
rewriteBasePathDeprecation,
cspRulesDeprecation,
];

View file

@ -0,0 +1,379 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ConfigDeprecationLogger } from './types';
import { configDeprecationFactory } from './deprecation_factory';
describe('DeprecationFactory', () => {
const { rename, unused, renameFromRoot, unusedFromRoot } = configDeprecationFactory;
let deprecationMessages: string[];
const logger: ConfigDeprecationLogger = msg => deprecationMessages.push(msg);
beforeEach(() => {
deprecationMessages = [];
});
describe('rename', () => {
it('moves the property to rename and logs a warning if old property exist and new one does not', () => {
const rawConfig = {
myplugin: {
deprecated: 'toberenamed',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger);
expect(processed).toEqual({
myplugin: {
renamed: 'toberenamed',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages).toMatchInlineSnapshot(`
Array [
"\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"",
]
`);
});
it('does not alter config and does not log if old property is not present', () => {
const rawConfig = {
myplugin: {
new: 'new',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = rename('deprecated', 'new')(rawConfig, 'myplugin', logger);
expect(processed).toEqual({
myplugin: {
new: 'new',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages.length).toEqual(0);
});
it('handles nested keys', () => {
const rawConfig = {
myplugin: {
oldsection: {
deprecated: 'toberenamed',
},
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = rename('oldsection.deprecated', 'newsection.renamed')(
rawConfig,
'myplugin',
logger
);
expect(processed).toEqual({
myplugin: {
oldsection: {},
newsection: {
renamed: 'toberenamed',
},
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages).toMatchInlineSnapshot(`
Array [
"\\"myplugin.oldsection.deprecated\\" is deprecated and has been replaced by \\"myplugin.newsection.renamed\\"",
]
`);
});
it('remove the old property but does not overrides the new one if they both exist, and logs a specific message', () => {
const rawConfig = {
myplugin: {
deprecated: 'deprecated',
renamed: 'renamed',
},
};
const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger);
expect(processed).toEqual({
myplugin: {
renamed: 'renamed',
},
});
expect(deprecationMessages).toMatchInlineSnapshot(`
Array [
"\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"",
]
`);
});
});
describe('renameFromRoot', () => {
it('moves the property from root and logs a warning if old property exist and new one does not', () => {
const rawConfig = {
myplugin: {
deprecated: 'toberenamed',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')(
rawConfig,
'does-not-matter',
logger
);
expect(processed).toEqual({
myplugin: {
renamed: 'toberenamed',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages).toMatchInlineSnapshot(`
Array [
"\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"",
]
`);
});
it('can move a property to a different namespace', () => {
const rawConfig = {
oldplugin: {
deprecated: 'toberenamed',
valid: 'valid',
},
newplugin: {
property: 'value',
},
};
const processed = renameFromRoot('oldplugin.deprecated', 'newplugin.renamed')(
rawConfig,
'does-not-matter',
logger
);
expect(processed).toEqual({
oldplugin: {
valid: 'valid',
},
newplugin: {
renamed: 'toberenamed',
property: 'value',
},
});
expect(deprecationMessages).toMatchInlineSnapshot(`
Array [
"\\"oldplugin.deprecated\\" is deprecated and has been replaced by \\"newplugin.renamed\\"",
]
`);
});
it('does not alter config and does not log if old property is not present', () => {
const rawConfig = {
myplugin: {
new: 'new',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = renameFromRoot('myplugin.deprecated', 'myplugin.new')(
rawConfig,
'does-not-matter',
logger
);
expect(processed).toEqual({
myplugin: {
new: 'new',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages.length).toEqual(0);
});
it('remove the old property but does not overrides the new one if they both exist, and logs a specific message', () => {
const rawConfig = {
myplugin: {
deprecated: 'deprecated',
renamed: 'renamed',
},
};
const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')(
rawConfig,
'does-not-matter',
logger
);
expect(processed).toEqual({
myplugin: {
renamed: 'renamed',
},
});
expect(deprecationMessages).toMatchInlineSnapshot(`
Array [
"\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"",
]
`);
});
});
describe('unused', () => {
it('removes the unused property from the config and logs a warning is present', () => {
const rawConfig = {
myplugin: {
deprecated: 'deprecated',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = unused('deprecated')(rawConfig, 'myplugin', logger);
expect(processed).toEqual({
myplugin: {
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages).toMatchInlineSnapshot(`
Array [
"myplugin.deprecated is deprecated and is no longer used",
]
`);
});
it('handles deeply nested keys', () => {
const rawConfig = {
myplugin: {
section: {
deprecated: 'deprecated',
},
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = unused('section.deprecated')(rawConfig, 'myplugin', logger);
expect(processed).toEqual({
myplugin: {
valid: 'valid',
section: {},
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages).toMatchInlineSnapshot(`
Array [
"myplugin.section.deprecated is deprecated and is no longer used",
]
`);
});
it('does not alter config and does not log if unused property is not present', () => {
const rawConfig = {
myplugin: {
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = unused('deprecated')(rawConfig, 'myplugin', logger);
expect(processed).toEqual({
myplugin: {
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages.length).toEqual(0);
});
});
describe('unusedFromRoot', () => {
it('removes the unused property from the root config and logs a warning is present', () => {
const rawConfig = {
myplugin: {
deprecated: 'deprecated',
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger);
expect(processed).toEqual({
myplugin: {
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages).toMatchInlineSnapshot(`
Array [
"myplugin.deprecated is deprecated and is no longer used",
]
`);
});
it('does not alter config and does not log if unused property is not present', () => {
const rawConfig = {
myplugin: {
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
};
const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger);
expect(processed).toEqual({
myplugin: {
valid: 'valid',
},
someOtherPlugin: {
property: 'value',
},
});
expect(deprecationMessages.length).toEqual(0);
});
});
});

View file

@ -0,0 +1,95 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { get, set } from 'lodash';
import { ConfigDeprecation, ConfigDeprecationLogger, ConfigDeprecationFactory } from './types';
import { unset } from '../../../utils';
const _rename = (
config: Record<string, any>,
rootPath: string,
log: ConfigDeprecationLogger,
oldKey: string,
newKey: string
) => {
const fullOldPath = getPath(rootPath, oldKey);
const oldValue = get(config, fullOldPath);
if (oldValue === undefined) {
return config;
}
unset(config, fullOldPath);
const fullNewPath = getPath(rootPath, newKey);
const newValue = get(config, fullNewPath);
if (newValue === undefined) {
set(config, fullNewPath, oldValue);
log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`);
} else {
log(
`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"`
);
}
return config;
};
const _unused = (
config: Record<string, any>,
rootPath: string,
log: ConfigDeprecationLogger,
unusedKey: string
) => {
const fullPath = getPath(rootPath, unusedKey);
if (get(config, fullPath) === undefined) {
return config;
}
unset(config, fullPath);
log(`${fullPath} is deprecated and is no longer used`);
return config;
};
const rename = (oldKey: string, newKey: string): ConfigDeprecation => (config, rootPath, log) =>
_rename(config, rootPath, log, oldKey, newKey);
const renameFromRoot = (oldKey: string, newKey: string): ConfigDeprecation => (
config,
rootPath,
log
) => _rename(config, '', log, oldKey, newKey);
const unused = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) =>
_unused(config, rootPath, log, unusedKey);
const unusedFromRoot = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) =>
_unused(config, '', log, unusedKey);
const getPath = (rootPath: string, subPath: string) =>
rootPath !== '' ? `${rootPath}.${subPath}` : subPath;
/**
* The actual platform implementation of {@link ConfigDeprecationFactory}
*
* @internal
*/
export const configDeprecationFactory: ConfigDeprecationFactory = {
rename,
renameFromRoot,
unused,
unusedFromRoot,
};

View file

@ -17,19 +17,13 @@
* under the License.
*/
import { createRoot } from '../../../../../test_utils/kbn_server';
(async function run() {
const root = createRoot(JSON.parse(process.env.CREATE_SERVER_OPTS));
// We just need the server to run through startup so that it will
// log the deprecation messages. Once it has started up we close it
// to allow the process to exit naturally
try {
await root.setup();
await root.start();
} finally {
await root.shutdown();
}
}());
export {
ConfigDeprecation,
ConfigDeprecationWithContext,
ConfigDeprecationLogger,
ConfigDeprecationFactory,
ConfigDeprecationProvider,
} from './types';
export { configDeprecationFactory } from './deprecation_factory';
export { coreDeprecationProvider } from './core_deprecations';
export { applyDeprecations } from './apply_deprecations';

View file

@ -0,0 +1,141 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Logger interface used when invoking a {@link ConfigDeprecation}
*
* @public
*/
export type ConfigDeprecationLogger = (message: string) => void;
/**
* Configuration deprecation returned from {@link ConfigDeprecationProvider} that handles a single deprecation from the configuration.
*
* @remarks
* This should only be manually implemented if {@link ConfigDeprecationFactory} does not provide the proper helpers for a specific
* deprecation need.
*
* @public
*/
export type ConfigDeprecation = (
config: Record<string, any>,
fromPath: string,
logger: ConfigDeprecationLogger
) => Record<string, any>;
/**
* A provider that should returns a list of {@link ConfigDeprecation}.
*
* See {@link ConfigDeprecationFactory} for more usage examples.
*
* @example
* ```typescript
* const provider: ConfigDeprecationProvider = ({ rename, unused }) => [
* rename('oldKey', 'newKey'),
* unused('deprecatedKey'),
* myCustomDeprecation,
* ]
* ```
*
* @public
*/
export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[];
/**
* Provides helpers to generates the most commonly used {@link ConfigDeprecation}
* when invoking a {@link ConfigDeprecationProvider}.
*
* See methods documentation for more detailed examples.
*
* @example
* ```typescript
* const provider: ConfigDeprecationProvider = ({ rename, unused }) => [
* rename('oldKey', 'newKey'),
* unused('deprecatedKey'),
* ]
* ```
*
* @public
*/
export interface ConfigDeprecationFactory {
/**
* Rename a configuration property from inside a plugin's configuration path.
* Will log a deprecation warning if the oldKey was found and deprecation applied.
*
* @example
* Rename 'myplugin.oldKey' to 'myplugin.newKey'
* ```typescript
* const provider: ConfigDeprecationProvider = ({ rename }) => [
* rename('oldKey', 'newKey'),
* ]
* ```
*/
rename(oldKey: string, newKey: string): ConfigDeprecation;
/**
* Rename a configuration property from the root configuration.
* Will log a deprecation warning if the oldKey was found and deprecation applied.
*
* This should be only used when renaming properties from different configuration's path.
* To rename properties from inside a plugin's configuration, use 'rename' instead.
*
* @example
* Rename 'oldplugin.key' to 'newplugin.key'
* ```typescript
* const provider: ConfigDeprecationProvider = ({ renameFromRoot }) => [
* renameFromRoot('oldplugin.key', 'newplugin.key'),
* ]
* ```
*/
renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation;
/**
* Remove a configuration property from inside a plugin's configuration path.
* Will log a deprecation warning if the unused key was found and deprecation applied.
*
* @example
* Flags 'myplugin.deprecatedKey' as unused
* ```typescript
* const provider: ConfigDeprecationProvider = ({ unused }) => [
* unused('deprecatedKey'),
* ]
* ```
*/
unused(unusedKey: string): ConfigDeprecation;
/**
* Remove a configuration property from the root configuration.
* Will log a deprecation warning if the unused key was found and deprecation applied.
*
* This should be only used when removing properties from outside of a plugin's configuration.
* To remove properties from inside a plugin's configuration, use 'unused' instead.
*
* @example
* Flags 'somepath.deprecatedProperty' as unused
* ```typescript
* const provider: ConfigDeprecationProvider = ({ unusedFromRoot }) => [
* unusedFromRoot('somepath.deprecatedProperty'),
* ]
* ```
*/
unusedFromRoot(unusedKey: string): ConfigDeprecation;
}
/** @internal */
export interface ConfigDeprecationWithContext {
deprecation: ConfigDeprecation;
path: string;
}

View file

@ -18,9 +18,16 @@
*/
export { ConfigService, IConfigService } from './config_service';
export { RawConfigService } from './raw_config_service';
export { RawConfigService, RawConfigurationProvider } from './raw_config_service';
export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config';
export { ObjectToConfigAdapter } from './object_to_config_adapter';
export { CliArgs, Env } from './env';
export {
ConfigDeprecation,
ConfigDeprecationLogger,
ConfigDeprecationProvider,
ConfigDeprecationFactory,
coreDeprecationProvider,
} from './deprecation';
export { EnvironmentMode, PackageInfo } from './types';

View file

@ -17,10 +17,9 @@
* under the License.
*/
import { transformDeprecations } from './transform_deprecations';
export function configDeprecationWarningsMixin(kbnServer, server) {
transformDeprecations(kbnServer.settings, (message) => {
server.log(['warning', 'config', 'deprecation'], message);
});
}
import { loggingServiceMock } from '../../logging/logging_service.mock';
export const mockLoggingService = loggingServiceMock.create();
mockLoggingService.asLoggerFactory.mockImplementation(() => mockLoggingService);
jest.doMock('../../logging/logging_service', () => ({
LoggingService: jest.fn(() => mockLoggingService),
}));

View file

@ -0,0 +1,64 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { mockLoggingService } from './config_deprecation.test.mocks';
import { loggingServiceMock } from '../../logging/logging_service.mock';
import * as kbnTestServer from '../../../../test_utils/kbn_server';
describe('configuration deprecations', () => {
let root: ReturnType<typeof kbnTestServer.createRoot>;
afterEach(async () => {
if (root) {
await root.shutdown();
}
});
it('should not log deprecation warnings for default configuration', async () => {
root = kbnTestServer.createRoot();
await root.setup();
const logs = loggingServiceMock.collect(mockLoggingService);
expect(logs.warn).toMatchInlineSnapshot(`Array []`);
});
it('should log deprecation warnings for core deprecations', async () => {
root = kbnTestServer.createRoot({
optimize: {
lazy: true,
lazyPort: 9090,
},
});
await root.setup();
const logs = loggingServiceMock.collect(mockLoggingService);
expect(logs.warn).toMatchInlineSnapshot(`
Array [
Array [
"\\"optimize.lazy\\" is deprecated and has been replaced by \\"optimize.watch\\"",
],
Array [
"\\"optimize.lazyPort\\" is deprecated and has been replaced by \\"optimize.watchPort\\"",
],
]
`);
});
});

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { RawConfigService } from './raw_config_service';
import { Observable, of } from 'rxjs';
const createRawConfigServiceMock = ({
rawConfig = {},
rawConfig$ = undefined,
}: { rawConfig?: Record<string, any>; rawConfig$?: Observable<Record<string, any>> } = {}) => {
const mocked: jest.Mocked<PublicMethodsOf<RawConfigService>> = {
loadConfig: jest.fn(),
stop: jest.fn(),
reloadConfig: jest.fn(),
getConfig$: jest.fn().mockReturnValue(rawConfig$ || of(rawConfig)),
};
return mocked;
};
export const rawConfigServiceMock = {
create: createRawConfigServiceMock,
};

View file

@ -88,8 +88,8 @@ test('returns config at path as observable', async () => {
.pipe(first())
.toPromise();
expect(exampleConfig.get('key')).toEqual('value');
expect(exampleConfig.getFlattenedPaths()).toEqual(['key']);
expect(exampleConfig.key).toEqual('value');
expect(Object.keys(exampleConfig)).toEqual(['key']);
});
test("pushes new configs when reloading even if config at path hasn't changed", async () => {
@ -110,19 +110,15 @@ test("pushes new configs when reloading even if config at path hasn't changed",
configService.reloadConfig();
expect(valuesReceived).toMatchInlineSnapshot(`
Array [
ObjectToConfigAdapter {
"rawConfig": Object {
"key": "value",
},
},
ObjectToConfigAdapter {
"rawConfig": Object {
"key": "value",
},
},
]
`);
Array [
Object {
"key": "value",
},
Object {
"key": "value",
},
]
`);
});
test('pushes new config when reloading and config at path has changed', async () => {
@ -143,10 +139,10 @@ test('pushes new config when reloading and config at path has changed', async ()
configService.reloadConfig();
expect(valuesReceived).toHaveLength(2);
expect(valuesReceived[0].get('key')).toEqual('value');
expect(valuesReceived[0].getFlattenedPaths()).toEqual(['key']);
expect(valuesReceived[1].get('key')).toEqual('new value');
expect(valuesReceived[1].getFlattenedPaths()).toEqual(['key']);
expect(valuesReceived[0].key).toEqual('value');
expect(Object.keys(valuesReceived[0])).toEqual(['key']);
expect(valuesReceived[1].key).toEqual('new value');
expect(Object.keys(valuesReceived[1])).toEqual(['key']);
});
test('completes config observables when stopped', done => {

View file

@ -22,10 +22,12 @@ import { Observable, ReplaySubject } from 'rxjs';
import { map } from 'rxjs/operators';
import typeDetect from 'type-detect';
import { Config } from './config';
import { ObjectToConfigAdapter } from './object_to_config_adapter';
import { getConfigFromFiles } from './read_config';
type RawConfigAdapter = (rawConfig: Record<string, any>) => Record<string, any>;
export type RawConfigurationProvider = Pick<RawConfigService, 'getConfig$'>;
/** @internal */
export class RawConfigService {
/**
@ -35,12 +37,11 @@ export class RawConfigService {
*/
private readonly rawConfigFromFile$: ReplaySubject<Record<string, any>> = new ReplaySubject(1);
private readonly config$: Observable<Config>;
private readonly config$: Observable<Record<string, any>>;
constructor(
public readonly configFiles: readonly string[],
configAdapter: (rawConfig: Record<string, any>) => Config = rawConfig =>
new ObjectToConfigAdapter(rawConfig)
configAdapter: RawConfigAdapter = rawConfig => rawConfig
) {
this.config$ = this.rawConfigFromFile$.pipe(
map(rawConfig => {

View file

@ -24,7 +24,7 @@ import { BehaviorSubject } from 'rxjs';
import { HttpService } from '.';
import { HttpConfigType, config } from './http_config';
import { httpServerMock } from './http_server.mocks';
import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config';
import { ConfigService, Env } from '../config';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { contextServiceMock } from '../context/context_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
@ -35,11 +35,12 @@ const coreId = Symbol();
const createConfigService = (value: Partial<HttpConfigType> = {}) => {
const configService = new ConfigService(
new BehaviorSubject<Config>(
new ObjectToConfigAdapter({
server: value,
})
),
{
getConfig$: () =>
new BehaviorSubject({
server: value,
}),
},
env,
logger
);

View file

@ -50,7 +50,16 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
export { bootstrap } from './bootstrap';
export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities';
export { ConfigPath, ConfigService, EnvironmentMode, PackageInfo } from './config';
export {
ConfigPath,
ConfigService,
ConfigDeprecation,
ConfigDeprecationProvider,
ConfigDeprecationLogger,
ConfigDeprecationFactory,
EnvironmentMode,
PackageInfo,
} from './config';
export {
IContextContainer,
IContextProvider,

View file

@ -50,7 +50,7 @@ describe('ensureValidConfiguration', () => {
coreHandledConfigPaths: ['core', 'elastic'],
pluginSpecs: 'pluginSpecs',
disabledPluginSpecs: 'disabledPluginSpecs',
inputSettings: 'settings',
settings: 'settings',
legacyConfig: 'pluginExtendedConfig',
});
});

View file

@ -30,7 +30,7 @@ export async function ensureValidConfiguration(
coreHandledConfigPaths: await configService.getUsedPaths(),
pluginSpecs,
disabledPluginSpecs,
inputSettings: settings,
settings,
legacyConfig: pluginExtendedConfig,
});

View file

@ -20,17 +20,10 @@
import { LegacyPluginSpec } from '../plugins/find_legacy_plugin_specs';
import { LegacyConfig } from './types';
import { getUnusedConfigKeys } from './get_unused_config_keys';
// @ts-ignore
import { transformDeprecations } from '../../../../legacy/server/config/transform_deprecations';
jest.mock('../../../../legacy/server/config/transform_deprecations', () => ({
transformDeprecations: jest.fn().mockImplementation(s => s),
}));
describe('getUnusedConfigKeys', () => {
beforeEach(() => {
jest.resetAllMocks();
transformDeprecations.mockImplementation((s: any) => s);
});
const getConfig = (values: Record<string, any> = {}): LegacyConfig =>
@ -45,7 +38,7 @@ describe('getUnusedConfigKeys', () => {
coreHandledConfigPaths: [],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {},
settings: {},
legacyConfig: getConfig(),
})
).toEqual([]);
@ -57,7 +50,7 @@ describe('getUnusedConfigKeys', () => {
coreHandledConfigPaths: [],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {
settings: {
presentInBoth: true,
alsoInBoth: 'someValue',
},
@ -75,7 +68,7 @@ describe('getUnusedConfigKeys', () => {
coreHandledConfigPaths: [],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {
settings: {
presentInBoth: true,
},
legacyConfig: getConfig({
@ -92,7 +85,7 @@ describe('getUnusedConfigKeys', () => {
coreHandledConfigPaths: [],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {
settings: {
presentInBoth: true,
onlyInSetting: 'value',
},
@ -109,7 +102,7 @@ describe('getUnusedConfigKeys', () => {
coreHandledConfigPaths: [],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {
settings: {
elasticsearch: {
username: 'foo',
password: 'bar',
@ -131,7 +124,7 @@ describe('getUnusedConfigKeys', () => {
coreHandledConfigPaths: [],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {
settings: {
env: 'development',
},
legacyConfig: getConfig({
@ -149,7 +142,7 @@ describe('getUnusedConfigKeys', () => {
coreHandledConfigPaths: [],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {
settings: {
prop: ['a', 'b', 'c'],
},
legacyConfig: getConfig({
@ -171,7 +164,7 @@ describe('getUnusedConfigKeys', () => {
getConfigPrefix: () => 'foo.bar',
} as unknown) as LegacyPluginSpec,
],
inputSettings: {
settings: {
foo: {
bar: {
unused: true,
@ -194,7 +187,7 @@ describe('getUnusedConfigKeys', () => {
coreHandledConfigPaths: ['core', 'foo.bar'],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {
settings: {
core: {
prop: 'value',
},
@ -209,46 +202,6 @@ describe('getUnusedConfigKeys', () => {
});
describe('using deprecation', () => {
it('calls transformDeprecations with the settings', async () => {
await getUnusedConfigKeys({
coreHandledConfigPaths: [],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {
prop: 'settings',
},
legacyConfig: getConfig({
prop: 'config',
}),
});
expect(transformDeprecations).toHaveBeenCalledTimes(1);
expect(transformDeprecations).toHaveBeenCalledWith({
prop: 'settings',
});
});
it('uses the transformed settings', async () => {
transformDeprecations.mockImplementation((settings: Record<string, any>) => {
delete settings.deprecated;
settings.updated = 'new value';
return settings;
});
expect(
await getUnusedConfigKeys({
coreHandledConfigPaths: [],
pluginSpecs: [],
disabledPluginSpecs: [],
inputSettings: {
onlyInSettings: 'bar',
deprecated: 'value',
},
legacyConfig: getConfig({
updated: 'config',
}),
})
).toEqual(['onlyInSettings']);
});
it('should use the plugin deprecations provider', async () => {
expect(
await getUnusedConfigKeys({
@ -262,7 +215,7 @@ describe('getUnusedConfigKeys', () => {
} as unknown) as LegacyPluginSpec,
],
disabledPluginSpecs: [],
inputSettings: {
settings: {
foo: {
foo: 'dolly',
foo1: 'bar',

View file

@ -19,8 +19,6 @@
import { difference, get, set } from 'lodash';
// @ts-ignore
import { transformDeprecations } from '../../../../legacy/server/config/transform_deprecations';
// @ts-ignore
import { getTransform } from '../../../../legacy/deprecation/index';
import { unset, getFlattenedObject } from '../../../../legacy/utils';
import { hasConfigPathIntersection } from '../../config';
@ -33,18 +31,15 @@ export async function getUnusedConfigKeys({
coreHandledConfigPaths,
pluginSpecs,
disabledPluginSpecs,
inputSettings,
settings,
legacyConfig,
}: {
coreHandledConfigPaths: string[];
pluginSpecs: LegacyPluginSpec[];
disabledPluginSpecs: LegacyPluginSpec[];
inputSettings: Record<string, any>;
settings: Record<string, any>;
legacyConfig: LegacyConfig;
}) {
// transform deprecated core settings
const settings = transformDeprecations(inputSettings);
// transform deprecated plugin settings
for (let i = 0; i < pluginSpecs.length; i++) {
const spec = pluginSpecs[i];

View file

@ -19,4 +19,10 @@
export { ensureValidConfiguration } from './ensure_valid_configuration';
export { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter';
export { LegacyConfig } from './types';
export { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters';
export {
LegacyConfig,
LegacyConfigDeprecation,
LegacyConfigDeprecationFactory,
LegacyConfigDeprecationProvider,
} from './types';

View file

@ -0,0 +1,106 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters';
import { LegacyConfigDeprecationProvider } from './types';
import { ConfigDeprecation } from '../../config';
import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory';
import { applyDeprecations } from '../../config/deprecation/apply_deprecations';
jest.spyOn(configDeprecationFactory, 'unusedFromRoot');
jest.spyOn(configDeprecationFactory, 'renameFromRoot');
const executeHandlers = (handlers: ConfigDeprecation[]) => {
handlers.forEach(handler => {
handler({}, '', () => null);
});
};
describe('convertLegacyDeprecationProvider', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('returns the same number of handlers', async () => {
const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [
rename('a', 'b'),
unused('c'),
unused('d'),
];
const migrated = await convertLegacyDeprecationProvider(legacyProvider);
const handlers = migrated(configDeprecationFactory);
expect(handlers).toHaveLength(3);
});
it('invokes the factory "unusedFromRoot" when using legacy "unused"', async () => {
const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [
rename('a', 'b'),
unused('c'),
unused('d'),
];
const migrated = await convertLegacyDeprecationProvider(legacyProvider);
const handlers = migrated(configDeprecationFactory);
executeHandlers(handlers);
expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledTimes(2);
expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledWith('c');
expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledWith('d');
});
it('invokes the factory "renameFromRoot" when using legacy "rename"', async () => {
const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [
rename('a', 'b'),
unused('c'),
rename('d', 'e'),
];
const migrated = await convertLegacyDeprecationProvider(legacyProvider);
const handlers = migrated(configDeprecationFactory);
executeHandlers(handlers);
expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledTimes(2);
expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledWith('a', 'b');
expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledWith('d', 'e');
});
it('properly works in a real use case', async () => {
const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [
rename('old', 'new'),
unused('unused'),
unused('notpresent'),
];
const convertedProvider = await convertLegacyDeprecationProvider(legacyProvider);
const handlers = convertedProvider(configDeprecationFactory);
const rawConfig = {
old: 'oldvalue',
unused: 'unused',
goodValue: 'good',
};
const migrated = applyDeprecations(
rawConfig,
handlers.map(handler => ({ deprecation: handler, path: '' }))
);
expect(migrated).toEqual({ new: 'oldvalue', goodValue: 'good' });
});
});

View file

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ConfigDeprecation, ConfigDeprecationProvider } from '../../config/deprecation';
import { LegacyConfigDeprecation, LegacyConfigDeprecationProvider } from './index';
import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory';
const convertLegacyDeprecation = (
legacyDeprecation: LegacyConfigDeprecation
): ConfigDeprecation => (config, fromPath, logger) => {
legacyDeprecation(config, logger);
return config;
};
const legacyUnused = (unusedKey: string): LegacyConfigDeprecation => (settings, log) => {
const deprecation = configDeprecationFactory.unusedFromRoot(unusedKey);
deprecation(settings, '', log);
};
const legacyRename = (oldKey: string, newKey: string): LegacyConfigDeprecation => (
settings,
log
) => {
const deprecation = configDeprecationFactory.renameFromRoot(oldKey, newKey);
deprecation(settings, '', log);
};
/**
* Async deprecation provider converter for legacy deprecation implementation
*
* @internal
*/
export const convertLegacyDeprecationProvider = async (
legacyProvider: LegacyConfigDeprecationProvider
): Promise<ConfigDeprecationProvider> => {
const legacyDeprecations = await legacyProvider({
rename: legacyRename,
unused: legacyUnused,
});
return () => legacyDeprecations.map(convertLegacyDeprecation);
};

View file

@ -17,7 +17,8 @@
* under the License.
*/
import { ConfigPath, ObjectToConfigAdapter } from '../../config';
import { ConfigPath } from '../../config';
import { ObjectToConfigAdapter } from '../../config/object_to_config_adapter';
/**
* Represents logging config supported by the legacy platform.

View file

@ -26,3 +26,33 @@ export interface LegacyConfig {
get<T>(key?: string): T;
has(key: string): boolean;
}
/**
* Representation of a legacy configuration deprecation factory used for
* legacy plugin deprecations.
*
* @internal
*/
export interface LegacyConfigDeprecationFactory {
rename(oldKey: string, newKey: string): LegacyConfigDeprecation;
unused(unusedKey: string): LegacyConfigDeprecation;
}
/**
* Representation of a legacy configuration deprecation.
*
* @internal
*/
export type LegacyConfigDeprecation = (
settings: Record<string, any>,
log: (msg: string) => void
) => void;
/**
* Representation of a legacy configuration deprecation provider.
*
* @internal
*/
export type LegacyConfigDeprecationProvider = (
factory: LegacyConfigDeprecationFactory
) => LegacyConfigDeprecation[] | Promise<LegacyConfigDeprecation[]>;

View file

@ -21,13 +21,9 @@ import { BehaviorSubject, throwError } from 'rxjs';
jest.mock('../../../legacy/server/kbn_server');
jest.mock('../../../cli/cluster/cluster_manager');
jest.mock('./plugins/find_legacy_plugin_specs.ts', () => ({
findLegacyPluginSpecs: (settings: Record<string, any>) => ({
pluginSpecs: [],
pluginExtendedConfig: settings,
disabledPluginSpecs: [],
uiExports: [],
}),
jest.mock('./plugins/find_legacy_plugin_specs');
jest.mock('./config/legacy_deprecation_adapters', () => ({
convertLegacyDeprecationProvider: (provider: any) => Promise.resolve(provider),
}));
import { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from '.';
@ -47,8 +43,10 @@ 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 { findLegacyPluginSpecs } from './plugins/find_legacy_plugin_specs';
const MockKbnServer: jest.Mock<KbnServer> = KbnServer as any;
const findLegacyPluginSpecsMock: jest.Mock<typeof findLegacyPluginSpecs> = findLegacyPluginSpecs as any;
let coreId: symbol;
let env: Env;
@ -66,6 +64,16 @@ beforeEach(() => {
env = Env.createDefault(getEnvOptions());
configService = configServiceMock.create();
findLegacyPluginSpecsMock.mockImplementation(
settings =>
Promise.resolve({
pluginSpecs: [],
pluginExtendedConfig: settings,
disabledPluginSpecs: [],
uiExports: [],
}) as any
);
MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve());
setupDeps = {
@ -115,6 +123,7 @@ beforeEach(() => {
afterEach(() => {
jest.clearAllMocks();
findLegacyPluginSpecsMock.mockReset();
});
describe('once LegacyService is set up with connection info', () => {
@ -382,3 +391,52 @@ test('Cannot start without setup phase', async () => {
`"Legacy service is not setup yet."`
);
});
describe('#discoverPlugins()', () => {
it('calls findLegacyPluginSpecs with correct parameters', async () => {
const legacyService = new LegacyService({
coreId,
env,
logger,
configService: configService as any,
});
await legacyService.discoverPlugins();
expect(findLegacyPluginSpecs).toHaveBeenCalledTimes(1);
expect(findLegacyPluginSpecs).toHaveBeenCalledWith(expect.any(Object), logger);
});
it(`register legacy plugin's deprecation providers`, async () => {
findLegacyPluginSpecsMock.mockImplementation(
settings =>
Promise.resolve({
pluginSpecs: [
{
getDeprecationsProvider: () => undefined,
},
{
getDeprecationsProvider: () => 'providerA',
},
{
getDeprecationsProvider: () => 'providerB',
},
],
pluginExtendedConfig: settings,
disabledPluginSpecs: [],
uiExports: [],
}) as any
);
const legacyService = new LegacyService({
coreId,
env,
logger,
configService: configService as any,
});
await legacyService.discoverPlugins();
expect(configService.addDeprecationProvider).toHaveBeenCalledTimes(2);
expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerA');
expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerB');
});
});

View file

@ -23,7 +23,7 @@ import { CoreService } from '../../types';
import { CoreSetup, CoreStart } from '../';
import { InternalCoreSetup, InternalCoreStart } from '../internal_types';
import { SavedObjectsLegacyUiExports } from '../types';
import { Config } from '../config';
import { Config, ConfigDeprecationProvider } from '../config';
import { CoreContext } from '../core_context';
import { DevConfig, DevConfigType } from '../dev';
import { BasePathProxyServer, HttpConfig, HttpConfigType } from '../http';
@ -32,7 +32,7 @@ import { PluginsServiceSetup, PluginsServiceStart } from '../plugins';
import { findLegacyPluginSpecs } from './plugins';
import { LegacyPluginSpec } from './plugins/find_legacy_plugin_specs';
import { PathConfigType } from '../path';
import { LegacyConfig } from './config';
import { LegacyConfig, convertLegacyDeprecationProvider } from './config';
interface LegacyKbnServer {
applyLoggingConfiguration: (settings: Readonly<Record<string, any>>) => void;
@ -157,6 +157,18 @@ export class LegacyService implements CoreService {
uiExports,
};
const deprecationProviders = await pluginSpecs
.map(spec => spec.getDeprecationsProvider())
.reduce(async (providers, current) => {
if (current) {
return [...(await providers), await convertLegacyDeprecationProvider(current)];
}
return providers;
}, Promise.resolve([] as ConfigDeprecationProvider[]));
deprecationProviders.forEach(provider =>
this.coreContext.configService.addDeprecationProvider('', provider)
);
this.legacyRawConfig = pluginExtendedConfig;
// check for unknown uiExport types

View file

@ -20,7 +20,7 @@
import { ServerExtType } from 'hapi';
import Podium from 'podium';
// @ts-ignore: implicit any for JS file
import { Config, transformDeprecations } from '../../../../legacy/server/config';
import { Config } from '../../../../legacy/server/config';
// @ts-ignore: implicit any for JS file
import { setupLogging } from '../../../../legacy/server/logging';
import { LogLevel } from '../../logging/log_level';
@ -99,7 +99,7 @@ export class LegacyLoggingServer {
ops: { interval: 2147483647 },
};
setupLogging(this, Config.withDefaultSchema(transformDeprecations(config)));
setupLogging(this, Config.withDefaultSchema(config));
}
public register({ plugin: { register }, options }: PluginRegisterParams): Promise<void> {

View file

@ -27,7 +27,7 @@ import {
import { LoggerFactory } from '../../logging';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports';
import { LegacyConfig } from '../config';
import { LegacyConfig, LegacyConfigDeprecationProvider } from '../config';
export interface LegacyPluginPack {
getPath(): string;
@ -37,6 +37,7 @@ export interface LegacyPluginSpec {
getId: () => unknown;
getExpectedKibanaVersion: () => string;
getConfigPrefix: () => string;
getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined;
}
export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: LoggerFactory) {

View file

@ -45,7 +45,7 @@ const createLoggingServiceMock = () => {
context,
...mockLog,
}));
mocked.asLoggerFactory.mockImplementation(() => createLoggingServiceMock());
mocked.asLoggerFactory.mockImplementation(() => mocked);
mocked.stop.mockResolvedValue();
return mocked;
};

View file

@ -20,9 +20,9 @@
import { mockPackage, mockReaddir, mockReadFile, mockStat } from './plugins_discovery.test.mocks';
import { resolve } from 'path';
import { BehaviorSubject } from 'rxjs';
import { first, map, toArray } from 'rxjs/operators';
import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../../config';
import { ConfigService, Env } from '../../config';
import { rawConfigServiceMock } from '../../config/raw_config_service.mock';
import { getEnvOptions } from '../../config/__mocks__/env';
import { loggingServiceMock } from '../../logging/logging_service.mock';
import { PluginWrapper } from '../plugin';
@ -115,9 +115,7 @@ test('properly iterates through plugin search locations', async () => {
})
);
const configService = new ConfigService(
new BehaviorSubject<Config>(
new ObjectToConfigAdapter({ plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } })
),
rawConfigServiceMock.create({ rawConfig: { plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } } }),
env,
logger
);

View file

@ -18,12 +18,12 @@
*/
import { duration } from 'moment';
import { BehaviorSubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { createPluginInitializerContext } from './plugin_context';
import { CoreContext } from '../core_context';
import { Env, ObjectToConfigAdapter } from '../config';
import { Env } from '../config';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { rawConfigServiceMock } from '../config/raw_config_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
import { PluginManifest } from './types';
import { Server } from '../server';
@ -54,9 +54,9 @@ describe('Plugin Context', () => {
beforeEach(async () => {
coreId = Symbol('core');
env = Env.createDefault(getEnvOptions());
const config$ = new BehaviorSubject(new ObjectToConfigAdapter({}));
const config$ = rawConfigServiceMock.create({ rawConfig: {} });
server = new Server(config$, env, logger);
await server.setupConfigSchemas();
await server.setupCoreConfig();
coreContext = { coreId, env, logger, configService: server.configService };
});

View file

@ -23,7 +23,8 @@ import { resolve, join } from 'path';
import { BehaviorSubject, from } from 'rxjs';
import { schema } from '@kbn/config-schema';
import { Config, ConfigPath, ConfigService, Env, ObjectToConfigAdapter } from '../config';
import { ConfigPath, ConfigService, Env } from '../config';
import { rawConfigServiceMock } from '../config/raw_config_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
import { coreMock } from '../mocks';
import { loggingServiceMock } from '../logging/logging_service.mock';
@ -38,7 +39,7 @@ import { DiscoveredPlugin } from './types';
const MockPluginsSystem: jest.Mock<PluginsSystem> = PluginsSystem as any;
let pluginsService: PluginsService;
let config$: BehaviorSubject<Config>;
let config$: BehaviorSubject<Record<string, any>>;
let configService: ConfigService;
let coreId: symbol;
let env: Env;
@ -109,10 +110,9 @@ describe('PluginsService', () => {
coreId = Symbol('core');
env = Env.createDefault(getEnvOptions());
config$ = new BehaviorSubject<Config>(
new ObjectToConfigAdapter({ plugins: { initialize: true } })
);
configService = new ConfigService(config$, env, logger);
config$ = new BehaviorSubject<Record<string, any>>({ plugins: { initialize: true } });
const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ });
configService = new ConfigService(rawConfigService, env, logger);
await configService.setSchema(config.path, config.schema);
pluginsService = new PluginsService({ coreId, env, logger, configService });
@ -388,6 +388,40 @@ describe('PluginsService', () => {
await pluginsService.discover();
expect(configService.setSchema).toBeCalledWith('path', configSchema);
});
it('registers plugin config deprecation provider in config service', async () => {
const configSchema = schema.string();
jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve());
jest.spyOn(configService, 'addDeprecationProvider');
const deprecationProvider = () => [];
jest.doMock(
join('path-with-provider', 'server'),
() => ({
config: {
schema: configSchema,
deprecations: deprecationProvider,
},
}),
{
virtual: true,
}
);
mockDiscover.mockReturnValue({
error$: from([]),
plugin$: from([
createPlugin('some-id', {
path: 'path-with-provider',
configPath: 'config-path',
}),
]),
});
await pluginsService.discover();
expect(configService.addDeprecationProvider).toBeCalledWith(
'config-path',
deprecationProvider
);
});
});
describe('#generateUiPluginsConfigs()', () => {
@ -499,9 +533,7 @@ describe('PluginsService', () => {
mockPluginSystem.uiPlugins.mockReturnValue(new Map());
config$.next(
new ObjectToConfigAdapter({ plugins: { initialize: true }, plugin1: { enabled: false } })
);
config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } });
await pluginsService.discover();
const { uiPlugins } = await pluginsService.setup({} as any);

View file

@ -196,6 +196,12 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
const configDescriptor = plugin.getConfigDescriptor();
if (configDescriptor) {
this.pluginConfigDescriptors.set(plugin.name, configDescriptor);
if (configDescriptor.deprecations) {
this.coreContext.configService.addDeprecationProvider(
plugin.configPath,
configDescriptor.deprecations
);
}
await this.coreContext.configService.setSchema(
plugin.configPath,
configDescriptor.schema

View file

@ -21,7 +21,7 @@ import { Observable } from 'rxjs';
import { Type } from '@kbn/config-schema';
import { RecursiveReadonly } from 'kibana/public';
import { ConfigPath, EnvironmentMode, PackageInfo } from '../config';
import { ConfigPath, EnvironmentMode, PackageInfo, ConfigDeprecationProvider } from '../config';
import { LoggerFactory } from '../logging';
import { KibanaConfigType } from '../kibana_config';
import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config';
@ -36,7 +36,7 @@ import { CoreSetup, CoreStart } from '..';
export type PluginConfigSchema<T> = Type<T>;
/**
* Describes a plugin configuration schema and capabilities.
* Describes a plugin configuration properties.
*
* @example
* ```typescript
@ -56,12 +56,20 @@ export type PluginConfigSchema<T> = Type<T>;
* uiProp: true,
* },
* schema: configSchema,
* deprecations: ({ rename, unused }) => [
* rename('securityKey', 'secret'),
* unused('deprecatedProperty'),
* ],
* };
* ```
*
* @public
*/
export interface PluginConfigDescriptor<T = any> {
/**
* Provider for the {@link ConfigDeprecation} to apply to the plugin configuration.
*/
deprecations?: ConfigDeprecationProvider;
/**
* List of configuration properties that will be available on the client-side plugin.
*/

View file

@ -29,8 +29,14 @@ jest.doMock('../config/config_service', () => ({
ConfigService: jest.fn(() => configService),
}));
import { rawConfigServiceMock } from '../config/raw_config_service.mock';
export const rawConfigService = rawConfigServiceMock.create();
jest.doMock('../config/raw_config_service', () => ({
RawConfigService: jest.fn(() => rawConfigService),
}));
export const mockServer = {
setupConfigSchemas: jest.fn(),
setupCoreConfig: jest.fn(),
setup: jest.fn(),
stop: jest.fn(),
configService,

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { configService, logger, mockServer } from './index.test.mocks';
import { rawConfigService, configService, logger, mockServer } from './index.test.mocks';
import { BehaviorSubject } from 'rxjs';
import { filter, first } from 'rxjs/operators';
@ -26,13 +26,13 @@ import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
const env = new Env('.', getEnvOptions());
const config$ = configService.getConfig$();
let mockConsoleError: jest.SpyInstance;
beforeEach(() => {
jest.spyOn(global.process, 'exit').mockReturnValue(undefined as never);
mockConsoleError = jest.spyOn(console, 'error').mockReturnValue(undefined);
rawConfigService.getConfig$.mockReturnValue(new BehaviorSubject({ someValue: 'foo' }));
configService.atPath.mockReturnValue(new BehaviorSubject({ someValue: 'foo' }));
});
@ -40,7 +40,7 @@ afterEach(() => {
jest.restoreAllMocks();
logger.asLoggerFactory.mockClear();
logger.stop.mockClear();
configService.getConfig$.mockClear();
rawConfigService.getConfig$.mockClear();
logger.upgrade.mockReset();
configService.atPath.mockReset();
@ -49,7 +49,7 @@ afterEach(() => {
});
test('sets up services on "setup"', async () => {
const root = new Root(config$, env);
const root = new Root(rawConfigService, env);
expect(logger.upgrade).not.toHaveBeenCalled();
expect(mockServer.setup).not.toHaveBeenCalled();
@ -65,7 +65,7 @@ test('upgrades logging configuration after setup', async () => {
const mockLoggingConfig$ = new BehaviorSubject({ someValue: 'foo' });
configService.atPath.mockReturnValue(mockLoggingConfig$);
const root = new Root(config$, env);
const root = new Root(rawConfigService, env);
await root.setup();
expect(logger.upgrade).toHaveBeenCalledTimes(1);
@ -80,7 +80,7 @@ test('upgrades logging configuration after setup', async () => {
test('stops services on "shutdown"', async () => {
const mockOnShutdown = jest.fn();
const root = new Root(config$, env, mockOnShutdown);
const root = new Root(rawConfigService, env, mockOnShutdown);
await root.setup();
@ -98,7 +98,7 @@ test('stops services on "shutdown"', async () => {
test('stops services on "shutdown" an calls `onShutdown` with error passed to `shutdown`', async () => {
const mockOnShutdown = jest.fn();
const root = new Root(config$, env, mockOnShutdown);
const root = new Root(rawConfigService, env, mockOnShutdown);
await root.setup();
@ -117,7 +117,7 @@ test('stops services on "shutdown" an calls `onShutdown` with error passed to `s
test('fails and stops services if server setup fails', async () => {
const mockOnShutdown = jest.fn();
const root = new Root(config$, env, mockOnShutdown);
const root = new Root(rawConfigService, env, mockOnShutdown);
const serverError = new Error('server failed');
mockServer.setup.mockRejectedValue(serverError);
@ -136,7 +136,7 @@ test('fails and stops services if server setup fails', async () => {
test('fails and stops services if initial logger upgrade fails', async () => {
const mockOnShutdown = jest.fn();
const root = new Root(config$, env, mockOnShutdown);
const root = new Root(rawConfigService, env, mockOnShutdown);
const loggingUpgradeError = new Error('logging config upgrade failed');
logger.upgrade.mockImplementation(() => {
@ -167,7 +167,7 @@ test('stops services if consequent logger upgrade fails', async () => {
const mockLoggingConfig$ = new BehaviorSubject({ someValue: 'foo' });
configService.atPath.mockReturnValue(mockLoggingConfig$);
const root = new Root(config$, env, mockOnShutdown);
const root = new Root(rawConfigService, env, mockOnShutdown);
await root.setup();
expect(mockOnShutdown).not.toHaveBeenCalled();

View file

@ -17,10 +17,10 @@
* under the License.
*/
import { ConnectableObservable, Observable, Subscription } from 'rxjs';
import { ConnectableObservable, Subscription } from 'rxjs';
import { first, map, publishReplay, switchMap, tap } from 'rxjs/operators';
import { Config, Env } from '../config';
import { Env, RawConfigurationProvider } from '../config';
import { Logger, LoggerFactory, LoggingConfigType, LoggingService } from '../logging';
import { Server } from '../server';
@ -35,19 +35,19 @@ export class Root {
private loggingConfigSubscription?: Subscription;
constructor(
config$: Observable<Config>,
rawConfigProvider: RawConfigurationProvider,
env: Env,
private readonly onShutdown?: (reason?: Error | string) => void
) {
this.loggingService = new LoggingService();
this.logger = this.loggingService.asLoggerFactory();
this.log = this.logger.get('root');
this.server = new Server(config$, env, this.logger);
this.server = new Server(rawConfigProvider, env, this.logger);
}
public async setup() {
try {
await this.server.setupConfigSchemas();
await this.server.setupCoreConfig();
await this.setupLogging();
this.log.debug('setting up root');
return await this.server.setup();

View file

@ -502,15 +502,34 @@ export class ClusterClient implements IClusterClient {
close(): void;
}
// @public
export type ConfigDeprecation = (config: Record<string, any>, fromPath: string, logger: ConfigDeprecationLogger) => Record<string, any>;
// @public
export interface ConfigDeprecationFactory {
rename(oldKey: string, newKey: string): ConfigDeprecation;
renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation;
unused(unusedKey: string): ConfigDeprecation;
unusedFromRoot(unusedKey: string): ConfigDeprecation;
}
// @public
export type ConfigDeprecationLogger = (message: string) => void;
// @public
export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[];
// @public (undocumented)
export type ConfigPath = string | string[];
// @internal (undocumented)
export class ConfigService {
// Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "RawConfigurationProvider" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Env" needs to be exported by the entry point index.d.ts
constructor(config$: Observable<Config>, env: Env, logger: LoggerFactory);
constructor(rawConfigProvider: RawConfigurationProvider, env: Env, logger: LoggerFactory);
addDeprecationProvider(path: ConfigPath, provider: ConfigDeprecationProvider): void;
atPath<TSchema>(path: ConfigPath): Observable<TSchema>;
// Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts
getConfig$(): Observable<Config>;
// (undocumented)
getUnusedPaths(): Promise<string[]>;
@ -520,6 +539,7 @@ export class ConfigService {
isEnabledAtPath(path: ConfigPath): Promise<boolean>;
optionalAtPath<TSchema>(path: ConfigPath): Observable<TSchema | undefined>;
setSchema(path: ConfigPath, schema: Type<unknown>): Promise<void>;
validate(): Promise<void>;
}
// @public
@ -1024,6 +1044,7 @@ export interface Plugin<TSetup = void, TStart = void, TPluginsSetup extends obje
// @public
export interface PluginConfigDescriptor<T = any> {
deprecations?: ConfigDeprecationProvider;
exposeToBrowser?: {
[P in keyof T]?: boolean;
};
@ -1792,9 +1813,9 @@ export const validBodyOutput: readonly ["data", "stream"];
//
// src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:213:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:213:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:214:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:215:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:222:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:223:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts
```

View file

@ -29,14 +29,16 @@ import {
} from './server.test.mocks';
import { BehaviorSubject } from 'rxjs';
import { Env, Config, ObjectToConfigAdapter } from './config';
import { Env } from './config';
import { Server } from './server';
import { getEnvOptions } from './config/__mocks__/env';
import { loggingServiceMock } from './logging/logging_service.mock';
import { rawConfigServiceMock } from './config/raw_config_service.mock';
const env = new Env('.', getEnvOptions());
const logger = loggingServiceMock.create();
const rawConfigService = rawConfigServiceMock.create({});
beforeEach(() => {
mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
@ -47,9 +49,8 @@ afterEach(() => {
jest.clearAllMocks();
});
const config$ = new BehaviorSubject<Config>(new ObjectToConfigAdapter({}));
test('sets up services on "setup"', async () => {
const server = new Server(config$, env, logger);
const server = new Server(rawConfigService, env, logger);
expect(mockHttpService.setup).not.toHaveBeenCalled();
expect(mockElasticsearchService.setup).not.toHaveBeenCalled();
@ -67,7 +68,7 @@ test('sets up services on "setup"', async () => {
});
test('injects legacy dependency to context#setup()', async () => {
const server = new Server(config$, env, logger);
const server = new Server(rawConfigService, env, logger);
const pluginA = Symbol();
const pluginB = Symbol();
@ -89,7 +90,7 @@ test('injects legacy dependency to context#setup()', async () => {
});
test('runs services on "start"', async () => {
const server = new Server(config$, env, logger);
const server = new Server(rawConfigService, env, logger);
expect(mockHttpService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.start).not.toHaveBeenCalled();
@ -109,13 +110,13 @@ test('runs services on "start"', async () => {
test('does not fail on "setup" if there are unused paths detected', async () => {
mockConfigService.getUnusedPaths.mockResolvedValue(['some.path', 'another.path']);
const server = new Server(config$, env, logger);
const server = new Server(rawConfigService, env, logger);
await expect(server.setup()).resolves.toBeDefined();
});
test('stops services on "stop"', async () => {
const server = new Server(config$, env, logger);
const server = new Server(rawConfigService, env, logger);
await server.setup();
@ -134,26 +135,25 @@ test('stops services on "stop"', async () => {
expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1);
});
test(`doesn't setup core services if services config validation fails`, async () => {
mockConfigService.setSchema.mockImplementation(() => {
throw new Error('invalid config');
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(config$, env, logger);
await expect(server.setupConfigSchemas()).rejects.toThrowErrorMatchingInlineSnapshot(
`"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();
});
test(`doesn't setup core services if config validation fails`, async () => {
test(`doesn't setup core services if legacy config validation fails`, async () => {
mockEnsureValidConfiguration.mockImplementation(() => {
throw new Error('Unknown configuration keys');
});
const server = new Server(config$, env, logger);
const server = new Server(rawConfigService, env, logger);
await expect(server.setup()).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unknown configuration keys"`

View file

@ -16,11 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { Type } from '@kbn/config-schema';
import { ConfigService, Env, Config, ConfigPath } from './config';
import {
ConfigService,
Env,
ConfigPath,
RawConfigurationProvider,
coreDeprecationProvider,
} from './config';
import { ElasticsearchService } from './elasticsearch';
import { HttpService, InternalHttpServiceSetup } from './http';
import { LegacyService, ensureValidConfiguration } from './legacy';
@ -44,6 +50,7 @@ import { InternalCoreSetup } from './internal_types';
import { CapabilitiesService } from './capabilities';
const coreId = Symbol('core');
const rootConfigPath = '';
export class Server {
public readonly configService: ConfigService;
@ -58,12 +65,12 @@ export class Server {
private readonly uiSettings: UiSettingsService;
constructor(
public readonly config$: Observable<Config>,
rawConfigProvider: RawConfigurationProvider,
public readonly env: Env,
private readonly logger: LoggerFactory
) {
this.log = this.logger.get('server');
this.configService = new ConfigService(config$, env, logger);
this.configService = new ConfigService(rawConfigProvider, env, logger);
const core = { coreId, configService: this.configService, env, logger };
this.context = new ContextService(core);
@ -84,6 +91,7 @@ export class Server {
const legacyPlugins = await this.legacy.discoverPlugins();
// Immediately terminate in case of invalid configuration
await this.configService.validate();
await ensureValidConfiguration(this.configService, legacyPlugins);
const contextServiceSetup = this.context.setup({
@ -207,7 +215,7 @@ export class Server {
);
}
public async setupConfigSchemas() {
public async setupCoreConfig() {
const schemas: Array<[ConfigPath, Type<unknown>]> = [
[pathConfig.path, pathConfig.schema],
[elasticsearchConfig.path, elasticsearchConfig.schema],
@ -220,6 +228,8 @@ export class Server {
[uiSettingsConfig.path, uiSettingsConfig.schema],
];
this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider);
for (const [path, schema] of schemas) {
await this.configService.setSchema(path, schema);
}

View file

@ -25,3 +25,4 @@ export * from './map_to_object';
export * from './merge';
export * from './pick';
export * from './url';
export * from './unset';

View file

@ -0,0 +1,104 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { unset } from './unset';
describe('unset', () => {
it('deletes a property from an object', () => {
const obj = {
a: 'a',
b: 'b',
c: 'c',
};
unset(obj, 'a');
expect(obj).toEqual({
b: 'b',
c: 'c',
});
});
it('does nothing if the property is not present', () => {
const obj = {
a: 'a',
b: 'b',
c: 'c',
};
unset(obj, 'd');
expect(obj).toEqual({
a: 'a',
b: 'b',
c: 'c',
});
});
it('handles nested paths', () => {
const obj = {
foo: {
bar: {
one: 'one',
two: 'two',
},
hello: 'dolly',
},
some: {
things: 'here',
},
};
unset(obj, 'foo.bar.one');
expect(obj).toEqual({
foo: {
bar: {
two: 'two',
},
hello: 'dolly',
},
some: {
things: 'here',
},
});
});
it('does nothing if nested paths does not exist', () => {
const obj = {
foo: {
bar: {
one: 'one',
two: 'two',
},
hello: 'dolly',
},
some: {
things: 'here',
},
};
unset(obj, 'foo.nothere.baz');
expect(obj).toEqual({
foo: {
bar: {
one: 'one',
two: 'two',
},
hello: 'dolly',
},
some: {
things: 'here',
},
});
});
});

49
src/core/utils/unset.ts Normal file
View file

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { get } from './get';
/**
* Unset a (potentially nested) key from given object.
* This mutates the original object.
*
* @example
* ```
* unset(myObj, 'someRootProperty');
* unset(myObj, 'some.nested.path');
* ```
*/
export function unset<OBJ extends { [k: string]: any }>(obj: OBJ, atPath: string) {
const paths = atPath
.split('.')
.map(s => s.trim())
.filter(v => v !== '');
if (paths.length === 0) {
return;
}
if (paths.length === 1) {
delete obj[paths[0]];
return;
}
const property = paths.pop() as string;
const parent = get(obj, paths as any) as any;
if (parent !== undefined) {
delete parent[property];
}
}

View file

@ -21,7 +21,7 @@ import * as Rx from 'rxjs';
import { distinct, toArray, mergeMap, share, shareReplay, filter, last, map, tap } from 'rxjs/operators';
import { realpathSync } from 'fs';
import { transformDeprecations, Config } from '../server/config';
import { Config } from '../server/config';
import {
extendConfigService,
@ -40,9 +40,7 @@ import {
} from './errors';
export function defaultConfig(settings) {
return Config.withDefaultSchema(
transformDeprecations(settings)
);
return Config.withDefaultSchema(settings);
}
function bufferAllResults(observable) {

View file

@ -19,7 +19,6 @@
import { get } from 'lodash';
import * as serverConfig from '../../server/config';
import { getTransform } from '../../deprecation';
/**
@ -33,7 +32,7 @@ import { getTransform } from '../../deprecation';
*/
export async function getSettings(spec, rootSettings, logDeprecation) {
const prefix = spec.getConfigPrefix();
const rawSettings = get(serverConfig.transformDeprecations(rootSettings), prefix);
const rawSettings = get(rootSettings, prefix);
const transform = await getTransform(spec);
return transform(rawSettings, logDeprecation);
}

View file

@ -1,116 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { spawn } from 'child_process';
import expect from '@kbn/expect';
const RUN_KBN_SERVER_STARTUP = require.resolve('./fixtures/run_kbn_server_startup');
const SETUP_NODE_ENV = require.resolve('../../../../setup_node_env');
const SECOND = 1000;
describe('config/deprecation warnings', function () {
this.timeout(65 * SECOND);
let stdio = '';
let proc = null;
before(async () => {
proc = spawn(process.execPath, [
'-r', SETUP_NODE_ENV,
RUN_KBN_SERVER_STARTUP
], {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
CREATE_SERVER_OPTS: JSON.stringify({
logging: {
quiet: false,
silent: false
},
uiSettings: {
enabled: true
}
})
}
});
// Either time out in 60 seconds, or resolve once the line is in our buffer
return Promise.race([
new Promise((resolve) => setTimeout(resolve, 60 * SECOND)),
new Promise((resolve, reject) => {
proc.stdout.on('data', (chunk) => {
stdio += chunk.toString('utf8');
if (chunk.toString('utf8').includes('deprecation')) {
resolve();
}
});
proc.stderr.on('data', (chunk) => {
stdio += chunk.toString('utf8');
if (chunk.toString('utf8').includes('deprecation')) {
resolve();
}
});
proc.on('exit', (code) => {
proc = null;
if (code > 0) {
reject(new Error(`Kibana server exited with ${code} -- stdout:\n\n${stdio}\n`));
} else {
resolve();
}
});
})
]);
});
after(() => {
if (proc) {
proc.kill('SIGKILL');
}
});
it('logs deprecation warnings when using outdated config', async () => {
const deprecationLines = stdio
.split('\n')
.map(json => {
try {
// in dev mode kibana might log things like node.js warnings which
// are not JSON, ignore the lines that don't parse as JSON
return JSON.parse(json);
} catch (error) {
return null;
}
})
.filter(Boolean)
.filter(line =>
line.type === 'log' &&
line.tags.includes('deprecation') &&
line.tags.includes('warning')
);
try {
expect(deprecationLines).to.have.length(1);
expect(deprecationLines[0]).to.have.property('message', 'uiSettings.enabled is deprecated and is no longer used');
} catch (error) {
throw new Error(`Expected stdio to include deprecation message about uiSettings.enabled\n\nstdio:\n${stdio}\n\n`);
}
});
});

View file

@ -17,5 +17,4 @@
* under the License.
*/
export { transformDeprecations } from './transform_deprecations';
export { Config } from './config';

View file

@ -1,127 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _, { set } from 'lodash';
import { createTransform, Deprecations } from '../../deprecation';
import { unset } from '../../utils';
const { rename, unused } = Deprecations;
const savedObjectsIndexCheckTimeout = (settings, log) => {
if (_.has(settings, 'savedObjects.indexCheckTimeout')) {
log('savedObjects.indexCheckTimeout is no longer necessary.');
if (Object.keys(settings.savedObjects).length > 1) {
delete settings.savedObjects.indexCheckTimeout;
} else {
delete settings.savedObjects;
}
}
};
const rewriteBasePath = (settings, log) => {
if (_.has(settings, 'server.basePath') && !_.has(settings, 'server.rewriteBasePath')) {
log(
'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' +
'will expect that all requests start with server.basePath rather than expecting you to rewrite ' +
'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' +
'current behavior and silence this warning.'
);
}
};
const loggingTimezone = (settings, log) => {
if (_.has(settings, 'logging.useUTC')) {
const timezone = settings.logging.useUTC ? 'UTC' : false;
set('logging.timezone', timezone);
unset(settings, 'logging.useUTC');
log(`Config key "logging.useUTC" is deprecated. It has been replaced with "logging.timezone"`);
}
};
const configPath = (settings, log) => {
if (_.has(process, 'env.CONFIG_PATH')) {
log(`Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder`);
}
};
const dataPath = (settings, log) => {
if (_.has(process, 'env.DATA_PATH')) {
log(`Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"`);
}
};
const NONCE_STRING = `{nonce}`;
// Policies that should include the 'self' source
const SELF_POLICIES = Object.freeze(['script-src', 'style-src']);
const SELF_STRING = `'self'`;
const cspRules = (settings, log) => {
const rules = _.get(settings, 'csp.rules');
if (!rules) {
return;
}
const parsed = new Map(rules.map(ruleStr => {
const parts = ruleStr.split(/\s+/);
return [parts[0], parts.slice(1)];
}));
settings.csp.rules = [...parsed].map(([policy, sourceList]) => {
if (sourceList.find(source => source.includes(NONCE_STRING))) {
log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`);
sourceList = sourceList.filter(source => !source.includes(NONCE_STRING));
// Add 'self' if not present
if (!sourceList.find(source => source.includes(SELF_STRING))) {
sourceList.push(SELF_STRING);
}
}
if (SELF_POLICIES.includes(policy) && !sourceList.find(source => source.includes(SELF_STRING))) {
log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`);
sourceList.push(SELF_STRING);
}
return `${policy} ${sourceList.join(' ')}`.trim();
});
};
const deprecations = [
//server
unused('server.xsrf.token'),
unused('uiSettings.enabled'),
rename('optimize.lazy', 'optimize.watch'),
rename('optimize.lazyPort', 'optimize.watchPort'),
rename('optimize.lazyHost', 'optimize.watchHost'),
rename('optimize.lazyPrebuild', 'optimize.watchPrebuild'),
rename('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'),
rename('xpack.telemetry.enabled', 'telemetry.enabled'),
rename('xpack.telemetry.config', 'telemetry.config'),
rename('xpack.telemetry.banner', 'telemetry.banner'),
rename('xpack.telemetry.url', 'telemetry.url'),
savedObjectsIndexCheckTimeout,
rewriteBasePath,
loggingTimezone,
configPath,
dataPath,
cspRules
];
export const transformDeprecations = createTransform(deprecations);

View file

@ -1,182 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import sinon from 'sinon';
import { transformDeprecations } from './transform_deprecations';
describe('server/config', function () {
describe('transformDeprecations', function () {
describe('savedObjects.indexCheckTimeout', () => {
it('removes the indexCheckTimeout and savedObjects properties', () => {
const settings = {
savedObjects: {
indexCheckTimeout: 123,
},
};
expect(transformDeprecations(settings)).toEqual({});
});
it('keeps the savedObjects property if it has other keys', () => {
const settings = {
savedObjects: {
indexCheckTimeout: 123,
foo: 'bar',
},
};
expect(transformDeprecations(settings)).toEqual({
savedObjects: {
foo: 'bar',
},
});
});
it('logs that the setting is no longer necessary', () => {
const settings = {
savedObjects: {
indexCheckTimeout: 123,
},
};
const log = sinon.spy();
transformDeprecations(settings, log);
sinon.assert.calledOnce(log);
sinon.assert.calledWithExactly(log, sinon.match('savedObjects.indexCheckTimeout'));
});
});
describe('csp.rules', () => {
describe('with nonce source', () => {
it('logs a warning', () => {
const settings = {
csp: {
rules: [`script-src 'self' 'nonce-{nonce}'`],
},
};
const log = jest.fn();
transformDeprecations(settings, log);
expect(log.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src",
],
]
`);
});
it('replaces a nonce', () => {
expect(
transformDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}'`] } }, jest.fn()).csp
.rules
).toEqual([`script-src 'self'`]);
expect(
transformDeprecations(
{ csp: { rules: [`script-src 'unsafe-eval' 'nonce-{nonce}'`] } },
jest.fn()
).csp.rules
).toEqual([`script-src 'unsafe-eval' 'self'`]);
});
it('removes a quoted nonce', () => {
expect(
transformDeprecations(
{ csp: { rules: [`script-src 'self' 'nonce-{nonce}'`] } },
jest.fn()
).csp.rules
).toEqual([`script-src 'self'`]);
expect(
transformDeprecations(
{ csp: { rules: [`script-src 'nonce-{nonce}' 'self'`] } },
jest.fn()
).csp.rules
).toEqual([`script-src 'self'`]);
});
it('removes a non-quoted nonce', () => {
expect(
transformDeprecations(
{ csp: { rules: [`script-src 'self' nonce-{nonce}`] } },
jest.fn()
).csp.rules
).toEqual([`script-src 'self'`]);
expect(
transformDeprecations(
{ csp: { rules: [`script-src nonce-{nonce} 'self'`] } },
jest.fn()
).csp.rules
).toEqual([`script-src 'self'`]);
});
it('removes a strange nonce', () => {
expect(
transformDeprecations(
{ csp: { rules: [`script-src 'self' blah-{nonce}-wow`] } },
jest.fn()
).csp.rules
).toEqual([`script-src 'self'`]);
});
it('removes multiple nonces', () => {
expect(
transformDeprecations(
{
csp: {
rules: [
`script-src 'nonce-{nonce}' 'self' blah-{nonce}-wow`,
`style-src 'nonce-{nonce}' 'self'`,
],
},
},
jest.fn()
).csp.rules
).toEqual([`script-src 'self'`, `style-src 'self'`]);
});
});
describe('without self source', () => {
it('logs a warning', () => {
const log = jest.fn();
transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, log);
expect(log.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"csp.rules must contain the 'self' source. Automatically adding to script-src.",
],
]
`);
});
it('adds self', () => {
expect(
transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, jest.fn()).csp
.rules
).toEqual([`script-src 'unsafe-eval' 'self'`]);
});
});
it('does not add self to other policies', () => {
expect(
transformDeprecations({ csp: { rules: [`worker-src blob:`] } }, jest.fn()).csp.rules
).toEqual([`worker-src blob:`]);
});
});
});
});

View file

@ -31,8 +31,6 @@ import { loggingMixin } from './logging';
import warningsMixin from './warnings';
import { statusMixin } from './status';
import pidMixin from './pid';
import { configDeprecationWarningsMixin } from './config/deprecation_warnings';
import { transformDeprecations } from './config/transform_deprecations';
import configCompleteMixin from './config/complete';
import optimizeMixin from '../../optimize';
import * as Plugins from './plugins';
@ -89,7 +87,6 @@ export default class KbnServer {
// adds methods for extending this.server
serverExtensionsMixin,
loggingMixin,
configDeprecationWarningsMixin,
warningsMixin,
statusMixin,
@ -198,7 +195,7 @@ export default class KbnServer {
applyLoggingConfiguration(settings) {
const config = new Config(
this.config.getSchema(),
transformDeprecations(settings)
settings
);
const loggingOptions = loggingConfiguration(config);

View file

@ -41,6 +41,11 @@ export const config: PluginConfigDescriptor = {
uiProp: true,
},
schema: configSchema,
deprecations: ({ rename, unused, renameFromRoot }) => [
rename('securityKey', 'secret'),
renameFromRoot('oldtestbed.uiProp', 'testbed.uiProp'),
unused('deprecatedProperty'),
],
};
class Plugin {

View file

@ -33,7 +33,6 @@ import { resolve } from 'path';
import { BehaviorSubject } from 'rxjs';
import supertest from 'supertest';
import { CliArgs, Env } from '../core/server/config';
import { LegacyObjectToConfigAdapter } from '../core/server/legacy';
import { Root } from '../core/server/root';
import KbnServer from '../legacy/server/kbn_server';
import { CallCluster } from '../legacy/core_plugins/elasticsearch';
@ -84,9 +83,9 @@ export function createRootWithSettings(
});
return new Root(
new BehaviorSubject(
new LegacyObjectToConfigAdapter(defaultsDeep({}, settings, DEFAULTS_SETTINGS))
),
{
getConfig$: () => new BehaviorSubject(defaultsDeep({}, settings, DEFAULTS_SETTINGS)),
},
env
);
}