From 0a8ba3b04b9d3d686a17ff2a7dbae1fa52c5df33 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Thu, 17 Oct 2019 14:40:19 +0200 Subject: [PATCH 1/7] Update http route registration in migration guide and examples --- src/core/MIGRATION.md | 16 +++-- src/core/MIGRATION_EXAMPLES.md | 123 +++++++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 9 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 09158295fde07c..8e49308e86e0d4 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -423,7 +423,7 @@ export default (kibana) => { } } - // HTTP functionality from core + // HTTP functionality from legacy server.route({ path: '/api/demo_plugin/search', method: 'POST', @@ -450,8 +450,13 @@ We now move this logic into a new plugin definition, which is based off of the c // server/plugin.ts import { ElasticsearchPlugin } from '../elasticsearch'; -interface CoreSetup { +// note: We use a name unique to our plugin for this type since our shimmed interface is not 100% +// compatible with NP's CoreSetup. +interface DemoPluginCoreSetup { elasticsearch: ElasticsearchPlugin // note: Elasticsearch is in Core in NP, rather than a plugin + http: { + route: Legacy.Server['route'] // note: NP uses `http.createRouter()` + } } interface FooSetup { @@ -465,7 +470,7 @@ interface PluginsSetup { export type DemoPluginSetup = ReturnType; export class Plugin { - public setup(core: CoreSetup, plugins: PluginsSetup) { + public setup(core: DemoPluginCoreSetup, plugins: PluginsSetup) { const serverFacade: ServerFacade = { plugins: { // We're still using the legacy Elasticsearch here, but we're now accessing it @@ -475,8 +480,9 @@ export class Plugin { } } - // HTTP functionality from legacy platform, accessed in the NP convention, just like Elasticsearch above. - core.http.route({ // note: we know routes will be created on core.http + // HTTP functionality from legacy platform, accessed in a way that's compatible with + // NP conventions even if not 100% the same + core.http.route({ path: '/api/demo_plugin/search', method: 'POST', async handler(request) { diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 79ae8953df0ea6..9639b73cddfd46 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -150,15 +150,18 @@ This interface has a different API with slightly different behaviors. not have native support for converting Boom exceptions into HTTP responses. ### Route Registration +Because of the incompatibility between the legacy and New Platform HTTP Route +API's it might be helpful to break up your migration work into several stages. +#### 1. Legacy route registration ```ts -// Legacy route registration +// legacy/plugins/myplugin/index.ts import Joi from 'joi'; new kibana.Plugin({ init(server) { server.route({ - path: '/api/my-plugin/my-route', + path: '/api/demoplugin/search', method: 'POST', options: { validate: { @@ -173,9 +176,89 @@ new kibana.Plugin({ }); } }); +``` +#### 2. New Platform shim using legacy router +Create a New Platform shim and inject the legacy `server.route` into your +plugin's setup function. This shim isn't exactly the same as the New +Platform's API's but it allows us to leave all of our route registration +untouched. -// New Platform equivalent +```ts +// legacy/plugins/demoplugin/index.ts +import { Plugin } from './server/plugin'; +export default (kibana) => { + return new kibana.Plugin({ + id: 'demo_plugin', + + init(server) { + // core shim + const coreSetup = { + http: { + route: server.route + }, + }; + + new Plugin().setup(coreSetup); + } + } +} +``` +```ts +// legacy/plugins/demoplugin/server/plugin.ts +export interface DemoPluginCoreSetup { + http: { + route: Legacy.Server['route']; + }; +}; +export class Plugin { + public setup(core: DemoPluginCoreSetup, plugins: DemoPluginsSetup) { + // HTTP functionality from legacy platform, accessed in a way that's + // compatible with NP conventions even if not 100% the same + core.http.route({ + path: '/api/demoplugin/search', + method: 'POST', + options: { + validate: { + payload: Joi.object({ + field1: Joi.string().required(), + }), + } + }, + async handler(req) { + return { message: `Received field1: ${req.payload.field1}` }; + }, + }); + } +} +``` + +#### 3. New Platform shim using New Platform router +We now switch our shim to use the real New Platform HTTP API's in our +`coreSetup` instead of relying on the legacy `server.route`. Since our plugin +is now using the New Platform API's we are guaranteed that our HTTP route +handling is 100% compatible with the New Platform. As a result, we will also +have adapt our route registration accordingly. +```ts +// legacy/plugins/demoplugin/index.ts +import { Plugin } from './server/plugin'; +export default (kibana) => { + return new kibana.Plugin({ + id: 'demo_plugin', + + init(server) { + // core shim + const coreSetup = { + http: server.newPlatform.setup.core.http, + }; + + new Plugin().setup(coreSetup); + } + } +} +``` +```ts +// legacy/plugins/demoplugin/server/plugin.ts import { schema } from '@kbn/config-schema'; class Plugin { @@ -183,7 +266,39 @@ class Plugin { const router = core.http.createRouter(); router.post( { - path: '/api/my-plugin/my-route', + path: '/api/demoplugin/search', + validate: { + body: schema.object({ + field1: schema.string(), + }), + } + }, + (context, req, res) => { + return res.ok({ + body: { + message: `Received field1: ${req.body.field1}` + } + }); + } + ) + } +} +``` + +#### 4. New Platform plugin +As the final step we delete the shim and move all our code into a New Platform +plugin. Since we were already consuming the New Platform API's no code changes +are necessary inside `plugin.ts`. +```ts +// plugins/demoplugin/server/plugin.ts +import { schema } from '@kbn/config-schema'; + +class Plugin { + public setup(core) { + const router = core.http.createRouter(); + router.post( + { + path: '/api/demoplugin/search', validate: { body: schema.object({ field1: schema.string(), From 47a2c52c6aa33696c787516487d5dd3fa2fd6c91 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Thu, 17 Oct 2019 14:53:53 +0200 Subject: [PATCH 2/7] Migration examples: added more types, table of contents --- src/core/MIGRATION_EXAMPLES.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 9639b73cddfd46..4a9a9a204e45d7 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -3,6 +3,20 @@ This document is a list of examples of how to migrate plugin code from legacy APIs to their New Platform equivalents. +- [Migration Examples](#migration-examples) + - [Configuration](#configuration) + - [Declaring config schema](#declaring-config-schema) + - [Using New Platform config from a Legacy plugin](#using-new-platform-config-from-a-legacy-plugin) + - [Create a New Platform plugin](#create-a-new-platform-plugin) + - [HTTP Routes](#http-routes) + - [Route Registration](#route-registration) + - [1. Legacy route registration](#1-legacy-route-registration) + - [2. New Platform shim using legacy router](#2-new-platform-shim-using-legacy-router) + - [3. New Platform shim using New Platform router](#3-new-platform-shim-using-new-platform-router) + - [4. New Platform plugin](#4-new-platform-plugin) + - [Accessing Services](#accessing-services) + - [Chrome](#chrome) + ## Configuration ### Declaring config schema @@ -186,14 +200,14 @@ untouched. ```ts // legacy/plugins/demoplugin/index.ts -import { Plugin } from './server/plugin'; +import { Plugin, DemoPluginCoreSetup } from './server/plugin'; export default (kibana) => { return new kibana.Plugin({ id: 'demo_plugin', init(server) { // core shim - const coreSetup = { + const coreSetup: DemoPluginCoreSetup = { http: { route: server.route }, @@ -206,11 +220,14 @@ export default (kibana) => { ``` ```ts // legacy/plugins/demoplugin/server/plugin.ts +import { Legacy } from 'kibana'; + export interface DemoPluginCoreSetup { http: { route: Legacy.Server['route']; }; }; + export class Plugin { public setup(core: DemoPluginCoreSetup, plugins: DemoPluginsSetup) { // HTTP functionality from legacy platform, accessed in a way that's @@ -234,11 +251,11 @@ export class Plugin { ``` #### 3. New Platform shim using New Platform router -We now switch our shim to use the real New Platform HTTP API's in our -`coreSetup` instead of relying on the legacy `server.route`. Since our plugin -is now using the New Platform API's we are guaranteed that our HTTP route -handling is 100% compatible with the New Platform. As a result, we will also -have adapt our route registration accordingly. +We now switch the shim to use the real New Platform HTTP API's in `coreSetup` +instead of relying on the legacy `server.route`. Since our plugin is now using +the New Platform API's we are guaranteed that our HTTP route handling is 100% +compatible with the New Platform. As a result, we will also have to adapt our +route registration accordingly. ```ts // legacy/plugins/demoplugin/index.ts import { Plugin } from './server/plugin'; @@ -260,9 +277,10 @@ export default (kibana) => { ```ts // legacy/plugins/demoplugin/server/plugin.ts import { schema } from '@kbn/config-schema'; +import { CoreSetup } from 'src/core/server'; class Plugin { - public setup(core) { + public setup(core: CoreSetup) { const router = core.http.createRouter(); router.post( { From 4f98d665f73ae2d06ac75f02bf274c70785f64c1 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Fri, 18 Oct 2019 14:39:08 +0200 Subject: [PATCH 3/7] Use __legacy namespace within CoreSetup --- src/core/MIGRATION.md | 137 ++++++++++++++++++++------------- src/core/MIGRATION_EXAMPLES.md | 26 +++---- 2 files changed, 97 insertions(+), 66 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 8e49308e86e0d4..55b6229028cdbb 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1,35 +1,56 @@ # Migrating legacy plugins to the new platform -* [Overview](#overview) - * [Architecture](#architecture) - * [Services](#services) - * [Integrating with other plugins](#integrating-with-other-plugins) - * [Challenges to overcome with legacy plugins](#challenges-to-overcome-with-legacy-plugins) - * [Plan of action](#plan-of-action) -* [Server-side plan of action](#server-side-plan-of-action) - * [De-couple from hapi.js server and request objects](#de-couple-from-hapijs-server-and-request-objects) - * [Introduce new plugin definition shim](#introduce-new-plugin-definition-shim) - * [Switch to new platform services](#switch-to-new-platform-services) - * [Migrate to the new plugin system](#migrate-to-the-new-plugin-system) -* [Browser-side plan of action](#browser-side-plan-of-action) - * [Move UI modules into plugins](#move-ui-modules-into-plugins) - * [Provide plugin extension points decoupled from angular.js](#provide-plugin-extension-points-decoupled-from-angularjs) - * [Move all webpack alias imports into uiExport entry files](#move-all-webpack-alias-imports-into-uiexport-entry-files) - * [Switch to new platform services](#switch-to-new-platform-services-1) - * [Migrate to the new plugin system](#migrate-to-the-new-plugin-system-1) -* [Frequently asked questions](#frequently-asked-questions) - * [Is migrating a plugin an all-or-nothing thing?](#is-migrating-a-plugin-an-all-or-nothing-thing) - * [Do plugins need to be converted to TypeScript?](#do-plugins-need-to-be-converted-to-typescript) - * [Can static code be shared between plugins?](#can-static-code-be-shared-between-plugins) - * [How can I avoid passing Core services deeply within my UI component tree?](#how-can-i-avoid-passing-core-services-deeply-within-my-ui-component-tree) - * [How is "common" code shared on both the client and server?](#how-is-common-code-shared-on-both-the-client-and-server) - * [When does code go into a plugin, core, or packages?](#when-does-code-go-into-a-plugin-core-or-packages) - * [How do I build my shim for New Platform services?](#how-do-i-build-my-shim-for-new-platform-services) - * aka, where did everything move to? -* [How to](#how-to) - * [Configure plugin](#configure-plugin) - * [Mock new platform services in tests](#mock-new-platform-services-in-tests) - * [Provide Legacy Platform API to the New platform plugin](#provide-legacy-platform-api-to-the-new-platform-plugin) +- [Migrating legacy plugins to the new platform](#migrating-legacy-plugins-to-the-new-platform) + - [Overview](#overview) + - [Architecture](#architecture) + - [Services](#services) + - [Integrating with other plugins](#integrating-with-other-plugins) + - [Challenges to overcome with legacy plugins](#challenges-to-overcome-with-legacy-plugins) + - [Challenges on the server](#challenges-on-the-server) + - [Challenges in the browser](#challenges-in-the-browser) + - [Plan of action](#plan-of-action) + - [Server-side plan of action](#server-side-plan-of-action) + - [De-couple from hapi.js server and request objects](#de-couple-from-hapijs-server-and-request-objects) + - [Introduce new plugin definition shim](#introduce-new-plugin-definition-shim) + - [Switch to new platform services](#switch-to-new-platform-services) + - [Migrate to the new plugin system](#migrate-to-the-new-plugin-system) + - [Browser-side plan of action](#browser-side-plan-of-action) + - [1. Create a plugin definition file](#1-create-a-plugin-definition-file) + - [2. Export all static code and types from `public/index.ts`](#2-export-all-static-code-and-types-from-publicindexts) + - [3. Export your runtime contract](#3-export-your-runtime-contract) + - [4. Move "owned" UI modules into your plugin and expose them from your public contract](#4-move-owned-ui-modules-into-your-plugin-and-expose-them-from-your-public-contract) + - [5. Provide plugin extension points decoupled from angular.js](#5-provide-plugin-extension-points-decoupled-from-angularjs) + - [6. Move all webpack alias imports into uiExport entry files](#6-move-all-webpack-alias-imports-into-uiexport-entry-files) + - [7. Switch to new platform services](#7-switch-to-new-platform-services) + - [8. Migrate to the new plugin system](#8-migrate-to-the-new-plugin-system) + - [Bonus: Tips for complex migration scenarios](#bonus-tips-for-complex-migration-scenarios) + - [Frequently asked questions](#frequently-asked-questions) + - [Is migrating a plugin an all-or-nothing thing?](#is-migrating-a-plugin-an-all-or-nothing-thing) + - [Do plugins need to be converted to TypeScript?](#do-plugins-need-to-be-converted-to-typescript) + - [Can static code be shared between plugins?](#can-static-code-be-shared-between-plugins) + - [Background](#background) + - [What goes wrong if I do share modules with state?](#what-goes-wrong-if-i-do-share-modules-with-state) + - [How to decide what code can be statically imported](#how-to-decide-what-code-can-be-statically-imported) + - [Concrete Example](#concrete-example) + - [How can I avoid passing Core services deeply within my UI component tree?](#how-can-i-avoid-passing-core-services-deeply-within-my-ui-component-tree) + - [How is "common" code shared on both the client and server?](#how-is-common-code-shared-on-both-the-client-and-server) + - [When does code go into a plugin, core, or packages?](#when-does-code-go-into-a-plugin-core-or-packages) + - [How do I build my shim for New Platform services?](#how-do-i-build-my-shim-for-new-platform-services) + - [Client-side](#client-side) + - [Core services](#core-services) + - [Plugins for shared application services](#plugins-for-shared-application-services) + - [Server-side](#server-side) + - [Core services](#core-services-1) + - [UI Exports](#ui-exports) + - [How to](#how-to) + - [Configure plugin](#configure-plugin) + - [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) + - [What about karma tests?](#what-about-karma-tests) + - [Provide Legacy Platform API to the New platform plugin](#provide-legacy-platform-api-to-the-new-platform-plugin) + - [On the server side](#on-the-server-side) + - [On the client side](#on-the-client-side) Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. @@ -448,13 +469,14 @@ We now move this logic into a new plugin definition, which is based off of the c ```ts // server/plugin.ts +import { CoreSetup } from 'src/core/server'; import { ElasticsearchPlugin } from '../elasticsearch'; -// note: We use a name unique to our plugin for this type since our shimmed interface is not 100% -// compatible with NP's CoreSetup. -interface DemoPluginCoreSetup { - elasticsearch: ElasticsearchPlugin // note: Elasticsearch is in Core in NP, rather than a plugin - http: { +// note: We use a name unique to our plugin for this type since our shimmed still references legacy +// dependencies which aren't compatible with NP's CoreSetup. +interface DemoPluginCoreSetup extends CoreSetup { + __legacy: { + elasticsearch: ElasticsearchPlugin, // note: Elasticsearch is in Core in NP, rather than a plugin route: Legacy.Server['route'] // note: NP uses `http.createRouter()` } } @@ -464,7 +486,9 @@ interface FooSetup { } interface PluginsSetup { - foo: FooSetup + __legacy: { + foo: FooSetup + } } export type DemoPluginSetup = ReturnType; @@ -476,13 +500,13 @@ export class Plugin { // We're still using the legacy Elasticsearch here, but we're now accessing it // the same way a NP plugin would, via core. Later, we'll swap this out for the // actual New Platform service. - elasticsearch: core.elasticsearch + elasticsearch: core.__legacy.elasticsearch } } // HTTP functionality from legacy platform, accessed in a way that's compatible with // NP conventions even if not 100% the same - core.http.route({ + core.__legacy.route({ path: '/api/demo_plugin/search', method: 'POST', async handler(request) { @@ -496,7 +520,7 @@ export class Plugin { // Exposing functionality for other plugins return { getDemoBar() { - return `Demo ${plugins.foo.getBar()}`; // Accessing functionality from another plugin + return `Demo ${plugins.__legacy.foo.getBar()}`; // Accessing functionality from another legacy plugin } }; } @@ -517,14 +541,17 @@ export default (kibana) => { init(server) { // core shim const coreSetup = { - elasticsearch: server.plugins.elasticsearch, - http: { + ...server.newPlatform.setup.core, + __legacy: { + elasticsearch: server.plugins.elasticsearch, route: server.route } }; // plugins shim const pluginsSetup = { - foo: server.plugins.foo + __legacy: { + foo: server.plugins.foo + } }; const demoSetup = new Plugin().setup(coreSetup, pluginsSetup); @@ -559,9 +586,8 @@ init(server) { // core shim const coreSetup = { ...server.newPlatform.setup.core, - - elasticsearch: server.plugins.elasticsearch, - http: { + __legacy: { + elasticsearch: server.plugins.elasticsearch, route: server.route } }; @@ -570,7 +596,7 @@ init(server) { If a legacy API differs from its new platform equivalent, some refactoring will be required. The best outcome comes from updating the plugin code to use the new API, but if that's not practical now, you can also create a facade inside your new plugin definition that is shaped like the legacy API but powered by the new API. Once either of these things is done, that override can be removed from the shim. -Eventually, all overrides will be removed and your `coreSetup` shim is entirely powered by `server.newPlatform.setup.core`. +Eventually, all `__legacy` dependencies will be removed and your `coreSetup` shim is entirely powered by `server.newPlatform.setup.core`. ```ts init(server) { @@ -590,12 +616,17 @@ init(server) { // plugins shim const pluginsSetup = { ...server.newPlatform.setup.plugins, - foo: server.plugins.foo + __legacy: { + foo: server.plugins.foo + } }; } ``` -As the plugins you depend on are migrated to the new platform, their contract will be exposed through `server.newPlatform`, so the legacy override should be removed. Like in core, plugins should take care to preserve their existing APIs to make this step as seamless as possible. +As the plugins you depend on are migrated to the new platform, their contract +will be exposed through `server.newPlatform`, so the `__legacy` dependencies +should be removed. Like in core, plugins should take care to preserve their +existing APIs to make this step as seamless as possible. It is much easier to reliably make breaking changes to plugin APIs in the new platform than it is in the legacy world, so if you're planning a big change, consider doing it after your dependent plugins have migrated rather than as part of your own migration. @@ -622,7 +653,7 @@ With the previous steps resolved, this final step should be easy, but the exact Other plugins may want to move subsystems over individually. For instance, you can move routes over to the New Platform in groups rather than all at once. Other examples that could be broken up: - Configuration schema ([see example](./MIGRATION_EXAMPLES.md#declaring-config-schema)) -- HTTP route registration +- HTTP route registration ([see example](./MIGRATION_EXAMPLES.md#route-registration)) - Polling mechanisms (eg. job worker) In general, we recommend moving all at once by ensuring you're not depending on any legacy code before you move over. @@ -1016,10 +1047,10 @@ If you have code that should be available to other plugins on both the client an There are some Core services that are purely presentational, for example `core.overlays.openModal()` or `core.application.createLink()` where UI code does need access to these deeply within your application. However, passing these services down as props throughout your application leads to lots of boilerplate. To avoid this, you have three options: 1. Use an abstraction layer, like Redux, to decouple your UI code from core (**this is the highly preferred option**); or - [redux-thunk](https://github.com/reduxjs/redux-thunk#injecting-a-custom-argument) and [redux-saga](https://redux-saga.js.org/docs/api/#createsagamiddlewareoptions) already have ways to do this. -1. Use React Context to provide these services to large parts of your React tree; or -1. Create a high-order-component that injects core into a React component; or +2. Use React Context to provide these services to large parts of your React tree; or +3. Create a high-order-component that injects core into a React component; or - This would be a stateful module that holds a reference to Core, but provides it as props to components with a `withCore(MyComponent)` interface. This can make testing components simpler. (Note: this module cannot be shared across plugin boundaries, see above). -1. Create a global singleton module that gets imported into each module that needs it. (Note: this module cannot be shared across plugin boundaries, see above). [Example](https://gist.github.com/epixa/06c8eeabd99da3c7545ab295e49acdc3). +4. Create a global singleton module that gets imported into each module that needs it. (Note: this module cannot be shared across plugin boundaries, see above). [Example](https://gist.github.com/epixa/06c8eeabd99da3c7545ab295e49acdc3). If you find that you need many different Core services throughout your application, this may be a code smell and could lead to pain down the road. For instance, if you need access to an HTTP Client or SavedObjectsClient in many places in your React tree, it's likely that a data layer abstraction (like Redux) could make developing your plugin much simpler (see option 1). @@ -1120,7 +1151,7 @@ In server code, `core` can be accessed from either `server.newPlatform` or `kbnS | Legacy Platform | New Platform | Notes | |----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| | `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | -| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | | +| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | | `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | | | `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client | | `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client | diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 4a9a9a204e45d7..8e111b353a92bc 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -9,10 +9,9 @@ APIs to their New Platform equivalents. - [Using New Platform config from a Legacy plugin](#using-new-platform-config-from-a-legacy-plugin) - [Create a New Platform plugin](#create-a-new-platform-plugin) - [HTTP Routes](#http-routes) - - [Route Registration](#route-registration) - - [1. Legacy route registration](#1-legacy-route-registration) - - [2. New Platform shim using legacy router](#2-new-platform-shim-using-legacy-router) - - [3. New Platform shim using New Platform router](#3-new-platform-shim-using-new-platform-router) + - [1. Legacy route registration](#1-legacy-route-registration) + - [2. New Platform shim using legacy router](#2-new-platform-shim-using-legacy-router) + - [3. New Platform shim using New Platform router](#3-new-platform-shim-using-new-platform-router) - [4. New Platform plugin](#4-new-platform-plugin) - [Accessing Services](#accessing-services) - [Chrome](#chrome) @@ -163,11 +162,10 @@ This interface has a different API with slightly different behaviors. continue using the `boom` module internally in your plugin, the framework does not have native support for converting Boom exceptions into HTTP responses. -### Route Registration Because of the incompatibility between the legacy and New Platform HTTP Route API's it might be helpful to break up your migration work into several stages. -#### 1. Legacy route registration +### 1. Legacy route registration ```ts // legacy/plugins/myplugin/index.ts import Joi from 'joi'; @@ -192,7 +190,7 @@ new kibana.Plugin({ }); ``` -#### 2. New Platform shim using legacy router +### 2. New Platform shim using legacy router Create a New Platform shim and inject the legacy `server.route` into your plugin's setup function. This shim isn't exactly the same as the New Platform's API's but it allows us to leave all of our route registration @@ -208,7 +206,8 @@ export default (kibana) => { init(server) { // core shim const coreSetup: DemoPluginCoreSetup = { - http: { + ...server.newPlatform.setup.core, + __legacy: { route: server.route }, }; @@ -220,10 +219,11 @@ export default (kibana) => { ``` ```ts // legacy/plugins/demoplugin/server/plugin.ts +import { CoreSetup } from 'src/core/server'; import { Legacy } from 'kibana'; -export interface DemoPluginCoreSetup { - http: { +export interface DemoPluginCoreSetup extends CoreSetup { + __legacy: { route: Legacy.Server['route']; }; }; @@ -232,7 +232,7 @@ export class Plugin { public setup(core: DemoPluginCoreSetup, plugins: DemoPluginsSetup) { // HTTP functionality from legacy platform, accessed in a way that's // compatible with NP conventions even if not 100% the same - core.http.route({ + core.__legacy.route({ path: '/api/demoplugin/search', method: 'POST', options: { @@ -250,7 +250,7 @@ export class Plugin { } ``` -#### 3. New Platform shim using New Platform router +### 3. New Platform shim using New Platform router We now switch the shim to use the real New Platform HTTP API's in `coreSetup` instead of relying on the legacy `server.route`. Since our plugin is now using the New Platform API's we are guaranteed that our HTTP route handling is 100% @@ -266,7 +266,7 @@ export default (kibana) => { init(server) { // core shim const coreSetup = { - http: server.newPlatform.setup.core.http, + ...server.newPlatform.setup.core, }; new Plugin().setup(coreSetup); From 8477048d9766773bbcd9090e48a6ed1f61c61c8c Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Fri, 18 Oct 2019 15:29:41 +0200 Subject: [PATCH 4/7] Inject legacy dependencies as third argument into plugins --- src/core/MIGRATION.md | 153 +++++++++++++++++---------------- src/core/MIGRATION_EXAMPLES.md | 68 +++++---------- 2 files changed, 99 insertions(+), 122 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 55b6229028cdbb..58c26261d490db 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -250,7 +250,7 @@ With that specified in the plugin manifest, the appropriate interfaces are then **demo plugin.ts:** ```ts -import { CoreSetup, CoreStart } from '../../../core/server'; +import { CoreSetup, CoreStart } from 'src/core/server'; import { FoobarPluginSetup, FoobarPluginStop } from '../../foobar/server'; interface DemoSetupPlugins { @@ -472,12 +472,14 @@ We now move this logic into a new plugin definition, which is based off of the c import { CoreSetup } from 'src/core/server'; import { ElasticsearchPlugin } from '../elasticsearch'; -// note: We use a name unique to our plugin for this type since our shimmed still references legacy -// dependencies which aren't compatible with NP's CoreSetup. -interface DemoPluginCoreSetup extends CoreSetup { - __legacy: { - elasticsearch: ElasticsearchPlugin, // note: Elasticsearch is in Core in NP, rather than a plugin - route: Legacy.Server['route'] // note: NP uses `http.createRouter()` +// We inject all legacy dependencies into our plugin including dependencies on other legacy plugins. +// Take care to only expose the legacy functionality you need e.g. don't inject the whole +// `Legacy.Server` if you only depend on `Legacy.Server['route']`. +interface LegacySetup { + route: Legacy.Server['route'] + plugins: { + elasticsearch: ElasticsearchPlugin, // note: Elasticsearch is in CoreSetup in NP, rather than a plugin + foo: FooSetup } } @@ -485,28 +487,27 @@ interface FooSetup { getBar(): string } -interface PluginsSetup { - __legacy: { - foo: FooSetup - } +export interface PluginsSetup { + // Once we start dependending on NP plugins we'll add their types here } -export type DemoPluginSetup = ReturnType; +export type DemoPluginSetup = { + getDemoBar: () => string; +} export class Plugin { - public setup(core: DemoPluginCoreSetup, plugins: PluginsSetup) { + public setup(core: CoreSetup, plugins: PluginsSetup, __LEGACY: LegacySetup) { + // We're still using the legacy Elasticsearch and http router here, but we're now accessing + // these services in the same way a NP plugin would: injected into the setup function. It's + // also obvious that these dependencies needs to be removed by migrating over to the New + // Platform services exposed through core. const serverFacade: ServerFacade = { plugins: { - // We're still using the legacy Elasticsearch here, but we're now accessing it - // the same way a NP plugin would, via core. Later, we'll swap this out for the - // actual New Platform service. - elasticsearch: core.__legacy.elasticsearch + elasticsearch: __LEGACY.plugins.elasticsearch } } - // HTTP functionality from legacy platform, accessed in a way that's compatible with - // NP conventions even if not 100% the same - core.__legacy.route({ + __LEGACY.route({ path: '/api/demo_plugin/search', method: 'POST', async handler(request) { @@ -520,7 +521,7 @@ export class Plugin { // Exposing functionality for other plugins return { getDemoBar() { - return `Demo ${plugins.__legacy.foo.getBar()}`; // Accessing functionality from another legacy plugin + return `Demo ${__LEGACY.plugins.foo.getBar()}`; // Accessing functionality from another legacy plugin } }; } @@ -532,29 +533,29 @@ The legacy plugin definition is still the one that is being executed, so we now ```ts // index.ts -import { Plugin } from './server/plugin'; +import { Plugin, PluginDependencies, LegacySetup } from './server/plugin'; export default (kibana) => { return new kibana.Plugin({ id: 'demo_plugin', init(server) { - // core shim - const coreSetup = { - ...server.newPlatform.setup.core, - __legacy: { + // core setup API's + const coreSetup = server.newPlatform.setup.core; + + // For now we don't have any dependencies on NP plugins + const pluginsSetup: PluginsSetup = {}; + + // legacy dependencies + const __LEGACY: LegacySetup = { + route: server.route, + plugins: { elasticsearch: server.plugins.elasticsearch, - route: server.route - } - }; - // plugins shim - const pluginsSetup = { - __legacy: { foo: server.plugins.foo } }; - const demoSetup = new Plugin().setup(coreSetup, pluginsSetup); + const demoSetup = new Plugin().setup(coreSetup, pluginsSetup, __LEGACY); // continue to expose functionality to legacy plugins server.expose('getDemoBar', demoSetup.getDemoBar); @@ -569,77 +570,81 @@ This introduces a layer between the legacy plugin system with hapi.js and the lo ### Switch to new platform services -At this point, your legacy server-side plugin is described in the shape and conventions of the new plugin system, and all of the touch points with the legacy world and hapi.js have been isolated to the shims in the legacy plugin definition. +At this point, your legacy server-side plugin is described in the shape and +conventions of the new plugin system, and all of the touch points with the +legacy world and hapi.js have been isolated inside the `__LEGACY` parameter. -Now the goal is to replace the legacy services backing your shims with services provided by the new platform instead. +Now the goal is to replace all legacy services with services provided by the new platform instead. For the first time in this guide, your progress here is limited by the migration efforts within core and other plugins. As core capabilities are migrated to services in the new platform, they are made available as lifecycle contracts to the legacy `init` function through `server.newPlatform`. This allows you to adopt the new platform service APIs directly in your legacy plugin as they get rolled out. -For the most part, care has been taken when migrating services to the new platform to preserve the existing APIs as much as possible, but there will be times when new APIs differ from the legacy equivalents. Start things off by having your core shim extend the equivalent new platform contract. - -```ts -// index.ts - -init(server) { - // core shim - const coreSetup = { - ...server.newPlatform.setup.core, - __legacy: { - elasticsearch: server.plugins.elasticsearch, - route: server.route - } - }; -} -``` +For the most part, care has been taken when migrating services to the new platform to preserve the existing APIs as much as possible, but there will be times when new APIs differ from the legacy equivalents. If a legacy API differs from its new platform equivalent, some refactoring will be required. The best outcome comes from updating the plugin code to use the new API, but if that's not practical now, you can also create a facade inside your new plugin definition that is shaped like the legacy API but powered by the new API. Once either of these things is done, that override can be removed from the shim. -Eventually, all `__legacy` dependencies will be removed and your `coreSetup` shim is entirely powered by `server.newPlatform.setup.core`. +Eventually, all `__LEGACY` dependencies will be removed and your Plugin will +be powered entirely by Core API's from `server.newPlatform.setup.core`. ```ts init(server) { - // core shim - const coreSetup = { - ...server.newPlatform.setup.core + // core setup API's + const coreSetup = server.newPlatform.setup.core; + + // For now we don't have any dependencies on NP plugins + const pluginsSetup: PluginsSetup = {}; + + // legacy dependencies, we've removed our dependency on elasticsearch and server.route + const __LEGACY: LegacySetup = { + plugins: { + foo: server.plugins.foo + } }; + + const demoSetup = new Plugin().setup(coreSetup, pluginsSetup, __LEGACY); } ``` -At this point, your legacy server-side plugin logic is no longer coupled to the legacy core. +At this point, your legacy server-side plugin logic is no longer coupled to +the legacy core. -A similar approach can be taken for your plugins shim. First, update your plugin shim in `init` to extend `server.newPlatform.setup.plugins`. +A similar approach can be taken for your plugin dependencies. To start +consuming an API from a New Platform plugin access these from +`server.newPlatform.setup.plugins` and inject it into your plugin's setup +function. ```ts init(server) { - // plugins shim - const pluginsSetup = { - ...server.newPlatform.setup.plugins, - __legacy: { - foo: server.plugins.foo - } + // core setup API's + const coreSetup = server.newPlatform.setup.core; + + // Depend on the NP plugin 'foo' + const pluginsSetup: PluginsSetup = { + foo: server.newPlatform.setup.plugins.foo }; + + const demoSetup = new Plugin().setup(coreSetup, pluginsSetup); } ``` As the plugins you depend on are migrated to the new platform, their contract -will be exposed through `server.newPlatform`, so the `__legacy` dependencies +will be exposed through `server.newPlatform`, so the `__LEGACY` dependencies should be removed. Like in core, plugins should take care to preserve their existing APIs to make this step as seamless as possible. -It is much easier to reliably make breaking changes to plugin APIs in the new platform than it is in the legacy world, so if you're planning a big change, consider doing it after your dependent plugins have migrated rather than as part of your own migration. +It is much easier to reliably make breaking changes to plugin APIs in the new +platform than it is in the legacy world, so if you're planning a big change, +consider doing it after your dependent plugins have migrated rather than as +part of your own migration. -Eventually, all overrides will be removed and your `pluginsSetup` shim is entirely powered by `server.newPlatform.setup.plugins`. +Eventually, all `__LEGACY` dependencies will be removed and your plugin will be +entirely powered by the New Platform and New Platform plugins. -```ts -init(server) { - // plugins shim - const pluginsSetup = { - ...server.newPlatform.setup.plugins - }; -} -``` +> Note: All New Platform plugins are exposed to legacy plugins via +> `server.newPlatform.setup.plugins`. Once you move your plugin over to the +> New Platform you will have to explicitly declare your dependencies on other +> plugins in your `kibana.json` manifest file. At this point, your legacy server-side plugin logic is no longer coupled to legacy plugins. diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 8e111b353a92bc..9eed3a59acaa62 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -192,27 +192,24 @@ new kibana.Plugin({ ### 2. New Platform shim using legacy router Create a New Platform shim and inject the legacy `server.route` into your -plugin's setup function. This shim isn't exactly the same as the New -Platform's API's but it allows us to leave all of our route registration -untouched. +plugin's setup function. ```ts // legacy/plugins/demoplugin/index.ts -import { Plugin, DemoPluginCoreSetup } from './server/plugin'; +import { Plugin, LegacySetup } from './server/plugin'; export default (kibana) => { return new kibana.Plugin({ id: 'demo_plugin', init(server) { // core shim - const coreSetup: DemoPluginCoreSetup = { - ...server.newPlatform.setup.core, - __legacy: { - route: server.route - }, + const coreSetup: server.newPlatform.setup.core; + const pluginSetup = {}; + const legacySetup: LegacySetup = { + route: server.route }; - new Plugin().setup(coreSetup); + new Plugin().setup(coreSetup, pluginSetup, legacySetup); } } } @@ -222,17 +219,15 @@ export default (kibana) => { import { CoreSetup } from 'src/core/server'; import { Legacy } from 'kibana'; -export interface DemoPluginCoreSetup extends CoreSetup { - __legacy: { - route: Legacy.Server['route']; - }; +export interface LegacySetup { + route: Legacy.Server['route']; }; +export interface DemoPluginsSetup {}; + export class Plugin { - public setup(core: DemoPluginCoreSetup, plugins: DemoPluginsSetup) { - // HTTP functionality from legacy platform, accessed in a way that's - // compatible with NP conventions even if not 100% the same - core.__legacy.route({ + public setup(core: CoreSetup, plugins: DemoPluginsSetup, __LEGACY: LegacySetup) { + __LEGACY.route({ path: '/api/demoplugin/search', method: 'POST', options: { @@ -265,11 +260,10 @@ export default (kibana) => { init(server) { // core shim - const coreSetup = { - ...server.newPlatform.setup.core, - }; + const coreSetup = server.newPlatform.setup.core; + const pluginSetup = {}; - new Plugin().setup(coreSetup); + new Plugin().setup(coreSetup, pluginSetup); } } } @@ -279,8 +273,10 @@ export default (kibana) => { import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; +export interface DemoPluginsSetup {}; + class Plugin { - public setup(core: CoreSetup) { + public setup(core: CoreSetup, pluginSetup: DemoPluginSetup) { const router = core.http.createRouter(); router.post( { @@ -308,31 +304,7 @@ As the final step we delete the shim and move all our code into a New Platform plugin. Since we were already consuming the New Platform API's no code changes are necessary inside `plugin.ts`. ```ts -// plugins/demoplugin/server/plugin.ts -import { schema } from '@kbn/config-schema'; - -class Plugin { - public setup(core) { - const router = core.http.createRouter(); - router.post( - { - path: '/api/demoplugin/search', - validate: { - body: schema.object({ - field1: schema.string(), - }), - } - }, - (context, req, res) => { - return res.ok({ - body: { - message: `Received field1: ${req.body.field1}` - } - }); - } - ) - } -} +// Move legacy/plugins/demoplugin/server/plugin.ts -> plugins/demoplugin/server/plugin.ts ``` ### Accessing Services From 8b05c2ae197a61e378bc68453d9d13ef910e58a9 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 21 Oct 2019 11:05:01 +0200 Subject: [PATCH 5/7] Legacy third argument for browserside and other updates --- src/core/MIGRATION.md | 61 ++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 58c26261d490db..93c71fe0bc36e5 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -469,9 +469,13 @@ We now move this logic into a new plugin definition, which is based off of the c ```ts // server/plugin.ts -import { CoreSetup } from 'src/core/server'; +import { CoreSetup, Plugin } from 'src/core/server'; import { ElasticsearchPlugin } from '../elasticsearch'; +interface FooSetup { + getBar(): string +} + // We inject all legacy dependencies into our plugin including dependencies on other legacy plugins. // Take care to only expose the legacy functionality you need e.g. don't inject the whole // `Legacy.Server` if you only depend on `Legacy.Server['route']`. @@ -483,18 +487,17 @@ interface LegacySetup { } } -interface FooSetup { - getBar(): string -} - -export interface PluginsSetup { - // Once we start dependending on NP plugins we'll add their types here -} - -export type DemoPluginSetup = { +// Define the public API's for our plugins setup and start lifecycle +export interface DemoSetup { getDemoBar: () => string; } +export interface DemoStart {} +// Once we start dependending on NP plugins' setup or start API's we'll add their types here +export interface DemoSetupDeps {} +export interface DemoStartDeps {} + +export class DemoPlugin implements Plugin { export class Plugin { public setup(core: CoreSetup, plugins: PluginsSetup, __LEGACY: LegacySetup) { // We're still using the legacy Elasticsearch and http router here, but we're now accessing @@ -563,6 +566,11 @@ export default (kibana) => { }); } ``` +> Note: An equally valid approach is to extend `CoreSetup` with a `__legacy` +> property instead of introducing a third parameter to your plugins lifecycle +> function. The important thing is that you reduce the legacy API surface that +> you depend on to a minimum by only picking and injecting the methods you +> require and that you clearly differentiate legacy dependencies in a namespace. This introduces a layer between the legacy plugin system with hapi.js and the logic you want to move to the new plugin system. The functionality exposed through that layer is still provided from the legacy world and in some cases is still technically powered directly by hapi, but building this layer forced you to identify the remaining touch points into the legacy world and it provides you with control when you start migrating to new platform-backed services. @@ -790,25 +798,17 @@ import { plugin } from '.'; import { setup as fooSetup, start as fooStart } from '../../foo/public/legacy'; // assumes `foo` lives in `legacy/core_plugins` const pluginInstance = plugin({} as PluginInitializerContext); -const shimCoreSetup = { - ...npSetup.core, - bar: {}, // shim for a core service that hasn't migrated yet -}; -const shimCoreStart = { - ...npStart.core, - bar: {}, -}; -const shimSetupPlugins = { - ...npSetup.plugins, - foo: fooSetup, +const __LEGACYSetup = { + bar: {}, // shim for a core service that hasn't migrated yet + foo: fooSetup, // dependency on a legacy plugin }; -const shimStartPlugins = { - ...npStart.plugins, - foo: fooStart, +const __LEGACYStart = { + bar: {}, // shim for a core service that hasn't migrated yet + foo: fooStart, // dependency on a legacy plugin }; -export const setup = pluginInstance.setup(shimCoreSetup, shimSetupPlugins); -export const start = pluginInstance.start(shimCoreStart, shimStartPlugins); +export const setup = pluginInstance.setup(npSetup.core, npSetup.plugins, __LEGACYSetup); +export const start = pluginInstance.start(npStart.core, npStart.plugins, __LEGACYStart); ``` > As you build your shims, you may be wondering where you will find some legacy services in the new platform. Skip to [the tables below](#how-do-i-build-my-shim-for-new-platform-services) for a list of some of the more common legacy services and where we currently expect them to live. @@ -877,16 +877,13 @@ import { uiThing } from 'ui/thing'; ... const pluginInstance = plugin({} as PluginInitializerContext); -const shimSetupPlugins = { - ...npSetup.plugins, +const __LEGACY = { foo: fooSetup, - __LEGACY: { - uiThing, // eventually this will move out of __LEGACY and into a proper plugin - }, + uiThing, // eventually this will move out of __LEGACY and into a NP plugin }; ... -export const setup = pluginInstance.setup(npSetup.core, shimSetupPlugins); +export const setup = pluginInstance.setup(npSetup.core, npSetup.plugins, __LEGACY); ``` #### 7. Switch to new platform services From c47f0f137237cd5eed87b09eef30ba5993740aa0 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 21 Oct 2019 11:53:30 +0200 Subject: [PATCH 6/7] Clarify comments --- src/core/MIGRATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 93c71fe0bc36e5..b12bfaa41a1741 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -476,8 +476,8 @@ interface FooSetup { getBar(): string } -// We inject all legacy dependencies into our plugin including dependencies on other legacy plugins. -// Take care to only expose the legacy functionality you need e.g. don't inject the whole +// We inject the miminal legacy dependencies into our plugin including dependencies on other legacy +// plugins. Take care to only expose the legacy functionality you need e.g. don't inject the whole // `Legacy.Server` if you only depend on `Legacy.Server['route']`. interface LegacySetup { route: Legacy.Server['route'] From 935555b8b5f8d1d018907ce15ae3b33ab0fa846d Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 21 Oct 2019 12:07:14 +0200 Subject: [PATCH 7/7] Edits --- src/core/MIGRATION.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index b12bfaa41a1741..36883440e0b5dc 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -498,8 +498,7 @@ export interface DemoSetupDeps {} export interface DemoStartDeps {} export class DemoPlugin implements Plugin { -export class Plugin { - public setup(core: CoreSetup, plugins: PluginsSetup, __LEGACY: LegacySetup) { + public setup(core: CoreSetup, plugins: PluginsSetup, __LEGACY: LegacySetup): DemoSetup { // We're still using the legacy Elasticsearch and http router here, but we're now accessing // these services in the same way a NP plugin would: injected into the setup function. It's // also obvious that these dependencies needs to be removed by migrating over to the New @@ -666,7 +665,7 @@ With the previous steps resolved, this final step should be easy, but the exact Other plugins may want to move subsystems over individually. For instance, you can move routes over to the New Platform in groups rather than all at once. Other examples that could be broken up: - Configuration schema ([see example](./MIGRATION_EXAMPLES.md#declaring-config-schema)) -- HTTP route registration ([see example](./MIGRATION_EXAMPLES.md#route-registration)) +- HTTP route registration ([see example](./MIGRATION_EXAMPLES.md#http-routes)) - Polling mechanisms (eg. job worker) In general, we recommend moving all at once by ensuring you're not depending on any legacy code before you move over.