diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 1ce18834f5319..a4fa3f17d0d94 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 6033c667c1866..3c4e33db4af91 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecation.md b/docs/development/core/server/kibana-plugin-server.configdeprecation.md new file mode 100644 index 0000000000000..ba7e40b8dc624 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecation.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [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. + +Signature: + +```typescript +export declare type ConfigDeprecation = (config: Record, fromPath: string, logger: ConfigDeprecationLogger) => Record; +``` + +## 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. + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md new file mode 100644 index 0000000000000..f022d6c1d064a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md @@ -0,0 +1,36 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [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. + +Signature: + +```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'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md new file mode 100644 index 0000000000000..5bbbad763c545 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md @@ -0,0 +1,36 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [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. + +Signature: + +```typescript +rename(oldKey: string, newKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| oldKey | string | | +| newKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Rename 'myplugin.oldKey' to 'myplugin.newKey' + +```typescript +const provider: ConfigDeprecationProvider = ({ rename }) => [ + rename('oldKey', 'newKey'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md new file mode 100644 index 0000000000000..d35ba25256fa1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md @@ -0,0 +1,38 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [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. + +Signature: + +```typescript +renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| oldKey | string | | +| newKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Rename 'oldplugin.key' to 'newplugin.key' + +```typescript +const provider: ConfigDeprecationProvider = ({ renameFromRoot }) => [ + renameFromRoot('oldplugin.key', 'newplugin.key'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md new file mode 100644 index 0000000000000..0381480e84c4d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md @@ -0,0 +1,35 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [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. + +Signature: + +```typescript +unused(unusedKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| unusedKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Flags 'myplugin.deprecatedKey' as unused + +```typescript +const provider: ConfigDeprecationProvider = ({ unused }) => [ + unused('deprecatedKey'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md new file mode 100644 index 0000000000000..ed37638b07375 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md @@ -0,0 +1,37 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [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. + +Signature: + +```typescript +unusedFromRoot(unusedKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| unusedKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Flags 'somepath.deprecatedProperty' as unused + +```typescript +const provider: ConfigDeprecationProvider = ({ unusedFromRoot }) => [ + unusedFromRoot('somepath.deprecatedProperty'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md b/docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md new file mode 100644 index 0000000000000..d2bb2a4e441b3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationLogger](./kibana-plugin-server.configdeprecationlogger.md) + +## ConfigDeprecationLogger type + +Logger interface used when invoking a [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) + +Signature: + +```typescript +export declare type ConfigDeprecationLogger = (message: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md b/docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md new file mode 100644 index 0000000000000..f5da9e9452bb5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [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. + +Signature: + +```typescript +export declare type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[]; +``` + +## Example + + +```typescript +const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + rename('oldKey', 'newKey'), + unused('deprecatedKey'), + myCustomDeprecation, +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 1506fdbb2b37f..06dcede0f2dfe 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -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 registerProvider method.Plugins having the responsibility to restrict capabilities depending on a given context should register their capabilities switcher using the registerSwitcher 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 setup method. | | [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start 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 PluginInitializer. | -| [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. | diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md new file mode 100644 index 0000000000000..00574101838f2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) > [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. + +Signature: + +```typescript +deprecations?: ConfigDeprecationProvider; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md index 41fdcfe5df45d..671298a67381a 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md @@ -4,7 +4,7 @@ ## PluginConfigDescriptor interface -Describes a plugin configuration schema and capabilities. +Describes a plugin configuration properties. Signature: @@ -16,6 +16,7 @@ export interface PluginConfigDescriptor | Property | Type | Description | | --- | --- | --- | +| [deprecations](./kibana-plugin-server.pluginconfigdescriptor.deprecations.md) | ConfigDeprecationProvider | Provider for the [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) to apply to the plugin configuration. | | [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | {
[P in keyof T]?: boolean;
} | List of configuration properties that will be available on the client-side plugin. | | [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | @@ -39,6 +40,10 @@ export const config: PluginConfigDescriptor = { uiProp: true, }, schema: configSchema, + deprecations: ({ rename, unused }) => [ + rename('securityKey', 'secret'), + unused('deprecatedProperty'), + ], }; ``` diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index 050d13b4b2c3e..f12a161fe2246 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -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 ); } diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 0e854cd8bb8ef..7c3fa4afad2ae 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -45,6 +45,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) @@ -1193,8 +1194,9 @@ In server code, `core` can be accessed from either `server.newPlatform` or `kbnS | `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) | | +| `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 | _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-server.coresetup.md)_ @@ -1371,6 +1373,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; + +export const config: PluginConfigDescriptor = { + 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 = { + 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 diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b745c23d52212..37c204a519801 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -886,7 +886,7 @@ export class SavedObjectsClient { bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index a0cf3d1602879..dff0c00a4625e 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -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()); diff --git a/src/core/server/config/config_service.mock.ts b/src/core/server/config/config_service.mock.ts index e87869e92deeb..b05b13d9e2d51 100644 --- a/src/core/server/config/config_service.mock.ts +++ b/src/core/server/config/config_service.mock.ts @@ -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$))); diff --git a/src/core/server/config/config_service.test.mocks.ts b/src/core/server/config/config_service.test.mocks.ts index 8fa1ec997d625..1299c4c0b4eb1 100644 --- a/src/core/server/config/config_service.test.mocks.ts +++ b/src/core/server/config/config_service.test.mocks.ts @@ -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, +})); diff --git a/src/core/server/config/config_service.test.ts b/src/core/server/config/config_service.test.ts index 131e1dd501792..773a444dea948 100644 --- a/src/core/server/config/config_service.test.ts +++ b/src/core/server/config/config_service.test.ts @@ -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) => + 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>({ 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>({ 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>({ 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", + ], + ] + `); +}); diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index c18a5b2000e01..61630f43bffb5 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -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; @@ -32,6 +40,9 @@ export type IConfigService = PublicMethodsOf; /** @internal */ export class ConfigService { private readonly log: Logger; + private readonly deprecationLog: Logger; + + private readonly config$: Observable; /** * 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>(); + private readonly deprecations = new BehaviorSubject([]); constructor( - private readonly config$: Observable, + 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(path: ConfigPath) { - return this.validateConfig(path) as Observable; + return this.validateConfigAtPath(path) as Observable; } /** @@ -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) { + 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) { 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) { diff --git a/src/core/server/config/deprecation/apply_deprecations.test.ts b/src/core/server/config/deprecation/apply_deprecations.test.ts new file mode 100644 index 0000000000000..25cae80d8b5cb --- /dev/null +++ b/src/core/server/config/deprecation/apply_deprecations.test.ts @@ -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' }); + }); +}); diff --git a/src/core/server/config/deprecation/apply_deprecations.ts b/src/core/server/config/deprecation/apply_deprecations.ts new file mode 100644 index 0000000000000..f7f95709ed846 --- /dev/null +++ b/src/core/server/config/deprecation/apply_deprecations.ts @@ -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, + deprecations: ConfigDeprecationWithContext[], + logger: ConfigDeprecationLogger = noopLogger +) => { + let processed = cloneDeep(config); + deprecations.forEach(({ deprecation, path }) => { + processed = deprecation(processed, path, logger); + }); + return processed; +}; diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts new file mode 100644 index 0000000000000..b40dbdc1b6651 --- /dev/null +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -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 = {}) => { + 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:`]); + }); + }); +}); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts new file mode 100644 index 0000000000000..6a401ec6625a2 --- /dev/null +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -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, +]; diff --git a/src/core/server/config/deprecation/deprecation_factory.test.ts b/src/core/server/config/deprecation/deprecation_factory.test.ts new file mode 100644 index 0000000000000..2595fdd923dd5 --- /dev/null +++ b/src/core/server/config/deprecation/deprecation_factory.test.ts @@ -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); + }); + }); +}); diff --git a/src/core/server/config/deprecation/deprecation_factory.ts b/src/core/server/config/deprecation/deprecation_factory.ts new file mode 100644 index 0000000000000..6f7ed4c4e84cc --- /dev/null +++ b/src/core/server/config/deprecation/deprecation_factory.ts @@ -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, + 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, + 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, +}; diff --git a/src/legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js b/src/core/server/config/deprecation/index.ts similarity index 63% rename from src/legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js rename to src/core/server/config/deprecation/index.ts index 3eaf18be609d4..f79338665166b 100644 --- a/src/legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js +++ b/src/core/server/config/deprecation/index.ts @@ -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'; diff --git a/src/core/server/config/deprecation/types.ts b/src/core/server/config/deprecation/types.ts new file mode 100644 index 0000000000000..19fba7800c919 --- /dev/null +++ b/src/core/server/config/deprecation/types.ts @@ -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, + fromPath: string, + logger: ConfigDeprecationLogger +) => Record; + +/** + * 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; +} diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index 491a24b2ab3d6..04dc402d35b22 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -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'; diff --git a/src/legacy/server/config/deprecation_warnings.js b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts similarity index 70% rename from src/legacy/server/config/deprecation_warnings.js rename to src/core/server/config/integration_tests/config_deprecation.test.mocks.ts index 06cd3ba7cf037..58b2da926b7c3 100644 --- a/src/legacy/server/config/deprecation_warnings.js +++ b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts @@ -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), +})); diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts new file mode 100644 index 0000000000000..e85f8567bfc68 --- /dev/null +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -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; + + 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\\"", + ], + ] + `); + }); +}); diff --git a/src/core/server/config/raw_config_service.mock.ts b/src/core/server/config/raw_config_service.mock.ts new file mode 100644 index 0000000000000..fdcb17395aaad --- /dev/null +++ b/src/core/server/config/raw_config_service.mock.ts @@ -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; rawConfig$?: Observable> } = {}) => { + const mocked: jest.Mocked> = { + loadConfig: jest.fn(), + stop: jest.fn(), + reloadConfig: jest.fn(), + getConfig$: jest.fn().mockReturnValue(rawConfig$ || of(rawConfig)), + }; + + return mocked; +}; + +export const rawConfigServiceMock = { + create: createRawConfigServiceMock, +}; diff --git a/src/core/server/config/raw_config_service.test.ts b/src/core/server/config/raw_config_service.test.ts index 361cef0d042ea..f02c31d4659ca 100644 --- a/src/core/server/config/raw_config_service.test.ts +++ b/src/core/server/config/raw_config_service.test.ts @@ -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 => { diff --git a/src/core/server/config/raw_config_service.ts b/src/core/server/config/raw_config_service.ts index b10137fb72f6a..728d793f494a9 100644 --- a/src/core/server/config/raw_config_service.ts +++ b/src/core/server/config/raw_config_service.ts @@ -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) => Record; + +export type RawConfigurationProvider = Pick; + /** @internal */ export class RawConfigService { /** @@ -35,12 +37,11 @@ export class RawConfigService { */ private readonly rawConfigFromFile$: ReplaySubject> = new ReplaySubject(1); - private readonly config$: Observable; + private readonly config$: Observable>; constructor( public readonly configFiles: readonly string[], - configAdapter: (rawConfig: Record) => Config = rawConfig => - new ObjectToConfigAdapter(rawConfig) + configAdapter: RawConfigAdapter = rawConfig => rawConfig ) { this.config$ = this.rawConfigFromFile$.pipe( map(rawConfig => { diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index eee8094417260..a2546709a318c 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -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 = {}) => { const configService = new ConfigService( - new BehaviorSubject( - new ObjectToConfigAdapter({ - server: value, - }) - ), + { + getConfig$: () => + new BehaviorSubject({ + server: value, + }), + }, env, logger ); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 51444a76f1737..c304958f78bb7 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -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, diff --git a/src/core/server/legacy/config/ensure_valid_configuration.test.ts b/src/core/server/legacy/config/ensure_valid_configuration.test.ts index 2997a9c8e7aff..d8917b46eba62 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.test.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.test.ts @@ -50,7 +50,7 @@ describe('ensureValidConfiguration', () => { coreHandledConfigPaths: ['core', 'elastic'], pluginSpecs: 'pluginSpecs', disabledPluginSpecs: 'disabledPluginSpecs', - inputSettings: 'settings', + settings: 'settings', legacyConfig: 'pluginExtendedConfig', }); }); diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/legacy/config/ensure_valid_configuration.ts index 8c76d45887761..026683a7b7cb0 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.ts @@ -30,7 +30,7 @@ export async function ensureValidConfiguration( coreHandledConfigPaths: await configService.getUsedPaths(), pluginSpecs, disabledPluginSpecs, - inputSettings: settings, + settings, legacyConfig: pluginExtendedConfig, }); diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts index 7b6be5368e769..bf011fa01a342 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts @@ -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 = {}): 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) => { - 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', diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index 22e7c50c0bc25..73cc7d8c50474 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -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; + settings: Record; 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]; diff --git a/src/core/server/legacy/config/index.ts b/src/core/server/legacy/config/index.ts index a837b8639939e..c3f308fd6d903 100644 --- a/src/core/server/legacy/config/index.ts +++ b/src/core/server/legacy/config/index.ts @@ -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'; diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts new file mode 100644 index 0000000000000..144e057c118f7 --- /dev/null +++ b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts @@ -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' }); + }); +}); diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.ts new file mode 100644 index 0000000000000..b0e3bc37e1510 --- /dev/null +++ b/src/core/server/legacy/config/legacy_deprecation_adapters.ts @@ -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 => { + const legacyDeprecations = await legacyProvider({ + rename: legacyRename, + unused: legacyUnused, + }); + return () => legacyDeprecations.map(convertLegacyDeprecation); +}; diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts index 8035596bb6072..75e1813f8c1f6 100644 --- a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts +++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts @@ -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. diff --git a/src/core/server/legacy/config/types.ts b/src/core/server/legacy/config/types.ts index cc4a6ac11fee4..24869e361c39c 100644 --- a/src/core/server/legacy/config/types.ts +++ b/src/core/server/legacy/config/types.ts @@ -26,3 +26,33 @@ export interface LegacyConfig { get(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, + log: (msg: string) => void +) => void; + +/** + * Representation of a legacy configuration deprecation provider. + * + * @internal + */ +export type LegacyConfigDeprecationProvider = ( + factory: LegacyConfigDeprecationFactory +) => LegacyConfigDeprecation[] | Promise; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 73b7ced60ee49..d4360c577d24c 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -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) => ({ - 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 as any; +const findLegacyPluginSpecsMock: jest.Mock = 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'); + }); +}); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index fcf0c45c17db8..4c2e57dc69b29 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -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>) => 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 diff --git a/src/core/server/legacy/logging/legacy_logging_server.ts b/src/core/server/legacy/logging/legacy_logging_server.ts index 0fe305fe77471..57706bcac2232 100644 --- a/src/core/server/legacy/logging/legacy_logging_server.ts +++ b/src/core/server/legacy/logging/legacy_logging_server.ts @@ -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 { diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 08ec1b004d7b4..0a49154801e56 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -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) { diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts index b5f522ca36a5f..50e6edc227bb5 100644 --- a/src/core/server/logging/logging_service.mock.ts +++ b/src/core/server/logging/logging_service.mock.ts @@ -45,7 +45,7 @@ const createLoggingServiceMock = () => { context, ...mockLog, })); - mocked.asLoggerFactory.mockImplementation(() => createLoggingServiceMock()); + mocked.asLoggerFactory.mockImplementation(() => mocked); mocked.stop.mockResolvedValue(); return mocked; }; diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 224259bc121ec..bf55fc7caae4c 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -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( - new ObjectToConfigAdapter({ plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } }) - ), + rawConfigServiceMock.create({ rawConfig: { plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } } }), env, logger ); diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 547ac08cca76d..3fcd7fbbbe1ff 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -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 }; }); diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index df5473bc97d99..6768e85c8db17 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -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 as any; let pluginsService: PluginsService; -let config$: BehaviorSubject; +let config$: BehaviorSubject>; 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( - new ObjectToConfigAdapter({ plugins: { initialize: true } }) - ); - configService = new ConfigService(config$, env, logger); + config$ = new BehaviorSubject>({ 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); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 3f9999aad4ab9..5a50cf8ea8ba2 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -196,6 +196,12 @@ export class PluginsService implements CoreService = Type; /** - * Describes a plugin configuration schema and capabilities. + * Describes a plugin configuration properties. * * @example * ```typescript @@ -56,12 +56,20 @@ export type PluginConfigSchema = Type; * uiProp: true, * }, * schema: configSchema, + * deprecations: ({ rename, unused }) => [ + * rename('securityKey', 'secret'), + * unused('deprecatedProperty'), + * ], * }; * ``` * * @public */ export interface PluginConfigDescriptor { + /** + * 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. */ diff --git a/src/core/server/root/index.test.mocks.ts b/src/core/server/root/index.test.mocks.ts index 5754e5a5b9321..1d3add66d7c22 100644 --- a/src/core/server/root/index.test.mocks.ts +++ b/src/core/server/root/index.test.mocks.ts @@ -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, diff --git a/src/core/server/root/index.test.ts b/src/core/server/root/index.test.ts index 4eba2133dce28..3b187aac022c3 100644 --- a/src/core/server/root/index.test.ts +++ b/src/core/server/root/index.test.ts @@ -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(); diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index ac6ef79483280..eecc6399366dc 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -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, + 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(); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 142332d613dc9..18e76324ff309 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -502,15 +502,34 @@ export class ClusterClient implements IClusterClient { close(): void; } +// @public +export type ConfigDeprecation = (config: Record, fromPath: string, logger: ConfigDeprecationLogger) => Record; + +// @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, env: Env, logger: LoggerFactory); + constructor(rawConfigProvider: RawConfigurationProvider, env: Env, logger: LoggerFactory); + addDeprecationProvider(path: ConfigPath, provider: ConfigDeprecationProvider): void; atPath(path: ConfigPath): Observable; + // Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts getConfig$(): Observable; // (undocumented) getUnusedPaths(): Promise; @@ -520,6 +539,7 @@ export class ConfigService { isEnabledAtPath(path: ConfigPath): Promise; optionalAtPath(path: ConfigPath): Observable; setSchema(path: ConfigPath, schema: Type): Promise; + validate(): Promise; } // @public @@ -1024,6 +1044,7 @@ export interface Plugin { + 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 ``` diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 7b91fb7257957..d593a6275fa4c 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -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(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"` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index e7166f30caa34..e7bc57ea5fb94 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -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, + 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]> = [ [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); } diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 98f0800feae79..b51cc4ef56410 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -25,3 +25,4 @@ export * from './map_to_object'; export * from './merge'; export * from './pick'; export * from './url'; +export * from './unset'; diff --git a/src/core/utils/unset.test.ts b/src/core/utils/unset.test.ts new file mode 100644 index 0000000000000..c0112e729811f --- /dev/null +++ b/src/core/utils/unset.test.ts @@ -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', + }, + }); + }); +}); diff --git a/src/core/utils/unset.ts b/src/core/utils/unset.ts new file mode 100644 index 0000000000000..8008d4ee08ba3 --- /dev/null +++ b/src/core/utils/unset.ts @@ -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: 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]; + } +} diff --git a/src/legacy/plugin_discovery/find_plugin_specs.js b/src/legacy/plugin_discovery/find_plugin_specs.js index faccdf396df04..e2a70b94c0010 100644 --- a/src/legacy/plugin_discovery/find_plugin_specs.js +++ b/src/legacy/plugin_discovery/find_plugin_specs.js @@ -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) { diff --git a/src/legacy/plugin_discovery/plugin_config/settings.js b/src/legacy/plugin_discovery/plugin_config/settings.js index d7d32ca04976a..44ecb5718fe21 100644 --- a/src/legacy/plugin_discovery/plugin_config/settings.js +++ b/src/legacy/plugin_discovery/plugin_config/settings.js @@ -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); } diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js deleted file mode 100644 index f49a1b6df45e2..0000000000000 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ /dev/null @@ -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`); - } - }); -}); diff --git a/src/legacy/server/config/index.js b/src/legacy/server/config/index.js index 7cc53ae1c74fb..0a83ecb1c58e1 100644 --- a/src/legacy/server/config/index.js +++ b/src/legacy/server/config/index.js @@ -17,5 +17,4 @@ * under the License. */ -export { transformDeprecations } from './transform_deprecations'; export { Config } from './config'; diff --git a/src/legacy/server/config/transform_deprecations.js b/src/legacy/server/config/transform_deprecations.js deleted file mode 100644 index b23a1de2c0773..0000000000000 --- a/src/legacy/server/config/transform_deprecations.js +++ /dev/null @@ -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 _ from 'lodash'; -import { createTransform, Deprecations } from '../../deprecation'; - -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 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, - configPath, - dataPath, - cspRules -]; - -export const transformDeprecations = createTransform(deprecations); diff --git a/src/legacy/server/config/transform_deprecations.test.js b/src/legacy/server/config/transform_deprecations.test.js deleted file mode 100644 index f8cf38efc8bd8..0000000000000 --- a/src/legacy/server/config/transform_deprecations.test.js +++ /dev/null @@ -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:`]); - }); - }); - }); -}); diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 87fb9dc8b9fec..ecd4dcfa14eb5 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -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); diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts index 07fda4eb98727..1b4de8f8f5c95 100644 --- a/src/plugins/testbed/server/index.ts +++ b/src/plugins/testbed/server/index.ts @@ -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 { diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index 0fc4c1f0d352e..40700e05bcde8 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -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 ); }