diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 168db2c4efb84..0744112650c23 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -249,14 +249,14 @@ #CC# /x-pack/plugins/translations/ @elastic/kibana-localization @elastic/kibana-core # Security -/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core +/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core /src/plugins/security_oss/ @elastic/kibana-security /src/plugins/spaces_oss/ @elastic/kibana-security /src/plugins/user_setup/ @elastic/kibana-security /test/security_functional/ @elastic/kibana-security -/x-pack/plugins/spaces/ @elastic/kibana-security -/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security -/x-pack/plugins/security/ @elastic/kibana-security +/x-pack/plugins/spaces/ @elastic/kibana-security +/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security +/x-pack/plugins/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security /x-pack/test/ui_capabilities/ @elastic/kibana-security /x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security @@ -265,6 +265,8 @@ /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security /x-pack/test/saved_object_api_integration/ @elastic/kibana-security +/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core +/examples/preboot_example/ @elastic/kibana-security @elastic/kibana-core #CC# /x-pack/plugins/security/ @elastic/kibana-security # Kibana Alerting Services diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index d9a8d0558714f..fe4c8a9280158 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -11,6 +11,7 @@ Some APM app features are provided via a REST API: * <> * <> * <> +* <> [float] [[apm-api-example]] @@ -72,6 +73,7 @@ curl -X POST \ //// ******************************************************* +******************************************************* //// [role="xpack"] @@ -202,11 +204,9 @@ DELETE /api/apm/settings/agent-configuration ******************************************************* //// - [[apm-list-config]] ==== List configuration - [[apm-list-config-req]] ===== Request @@ -274,7 +274,6 @@ GET /api/apm/settings/agent-configuration ******************************************************* //// - [[apm-search-config]] ==== Search configuration @@ -472,6 +471,7 @@ curl -X POST \ //// ******************************************************* +******************************************************* //// [[kibana-api]] @@ -553,3 +553,239 @@ The API returns the following: // More examples will go here More information on Kibana's API is available in <>. + +//// +******************************************************* +******************************************************* +//// + +[role="xpack"] +[[rum-sourcemap-api]] +=== RUM source map API + +IMPORTANT: This endpoint is only compatible with the +{apm-server-ref}/apm-integration.html[APM integration for Elastic Agent]. +Users with a standalone APM Server should instead use the APM Server +{apm-server-ref}/sourcemap-api.html[source map upload API]. + +A source map allows minified files to be mapped back to original source code -- +allowing you to maintain the speed advantage of minified code, +without losing the ability to quickly and easily debug your application. + +For best results, uploading source maps should become a part of your deployment procedure, +and not something you only do when you see unhelpful errors. +That’s because uploading source maps after errors happen won’t make old errors magically readable -- +errors must occur again for source mapping to occur. + +The following APIs are available: + +* <> +* <> +* <> + +[float] +[[use-sourcemap-api]] +==== How to use APM APIs + +.Expand for required headers, privileges, and usage details +[%collapsible%closed] +====== +include::api.asciidoc[tag=using-the-APIs] +====== + +//// +******************************************************* +//// + +[[rum-sourcemap-post]] +==== Create or update source map + +Create or update a source map for a specific service and version. + +[[rum-sourcemap-post-privs]] +===== Privileges + +The user accessing this endpoint requires `All` Kibana privileges for the {beat_kib_app} feature. +For more information, see <>. + +[[apm-sourcemap-post-req]] +===== Request + +`POST /api/apm/sourcemaps` + +[role="child_attributes"] +[[apm-sourcemap-post-req-body]] +===== Request body + +`service_name`:: +(required, string) The name of the service that the service map should apply to. + +`service_version`:: +(required, string) The version of the service that the service map should apply to. + +`bundle_filepath`:: +(required, string) The absolute path of the final bundle as used in the web application. + +`sourcemap`:: +(required, string or file upload) The source map. It must follow the +https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k[source map revision 3 proposal]. + +[[apm-sourcemap-post-example]] +===== Examples + +The following example uploads a source map for a service named `foo` and a service version of `1.0.0`: + +[source,curl] +-------------------------------------------------- +curl -X POST "http://localhost:5601/api/apm/sourcemaps" \ +-H 'Content-Type: multipart/form-data' \ +-H 'kbn-xsrf: true' \ +-H 'Authorization: ApiKey ${YOUR_API_KEY}' \ +-F 'service_name="foo"' \ +-F 'service_version="1.0.0"' \ +-F 'bundle_filepath="/test/e2e/general-usecase/bundle.js.map"' \ +-F 'sourcemap="{\"version\":3,\"file\":\"static/js/main.chunk.js\",\"sources\":[\"fleet-source-map-client/src/index.css\",\"fleet-source-map-client/src/App.js\",\"webpack:///./src/index.css?bb0a\",\"fleet-source-map-client/src/index.js\",\"fleet-source-map-client/src/reportWebVitals.js\"],\"sourcesContent\":[\"content\"],\"mappings\":\"mapping\",\"sourceRoot\":\"\"}"' <1> +-------------------------------------------------- +<1> Alternatively, upload the source map as a file with `-F 'sourcemap=@path/to/source_map/bundle.js.map'` + +[[apm-sourcemap-post-body]] +===== Response body + +[source,js] +-------------------------------------------------- +{ + "type": "sourcemap", + "identifier": "foo-1.0.0", + "relative_url": "/api/fleet/artifacts/foo-1.0.0/644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", + "body": "eJyFkL1OwzAUhd/Fc+MbYMuCEBIbHRjKgBgc96R16tiWr1OQqr47NwqJxEK3q/PzWccXxchnZ7E1A1SjuhjVZtF2yOxiEPlO17oWox3D3uPFeSRTjmJQARfCPeiAgGx8NTKsYdAc1T3rwaSJGcds8Sp3c1HnhfywUZ3QhMTFFGepZxqMC9oex3CS9tpk1XyozgOlmoVKuJX1DqEQZ0su7PGtLU+V/3JPKc3cL7TJ2FNDRPov4bFta3MDM4f7W69lpJjLO9qdK8bzVPhcJz3HUCQ4LbO/p5hCSC4cZPByrp/wFqOklbpefwAhzpqI", + "created": "2021-07-09T20:47:44.812Z", + "id": "apm:foo-1.0.0-644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", + "compressionAlgorithm": "zlib", + "decodedSha256": "644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", + "decodedSize": 441, + "encodedSha256": "024c72749c3e3dd411b103f7040ae62633558608f480bce4b108cf5b2275bd24", + "encodedSize": 237, + "encryptionAlgorithm": "none", + "packageName": "apm" +} +-------------------------------------------------- + +//// +******************************************************* +//// + +[[rum-sourcemap-get]] +==== Get source maps + +Returns an array of Fleet artifacts, including source map uploads. + +[[rum-sourcemap-get-privs]] +===== Privileges + +The user accessing this endpoint requires `Read` or `All` Kibana privileges for the {beat_kib_app} feature. +For more information, see <>. + +[[apm-sourcemap-get-req]] +===== Request + +`GET /api/apm/sourcemaps` + +[[apm-sourcemap-get-example]] +===== Example + +The following example requests all uploaded source maps: + +[source,curl] +-------------------------------------------------- +curl -X GET "http://localhost:5601/api/apm/sourcemaps" \ +-H 'Content-Type: application/json' \ +-H 'kbn-xsrf: true' \ +-H 'Authorization: ApiKey ${YOUR_API_KEY}' +-------------------------------------------------- + +[[apm-sourcemap-get-body]] +===== Response body + +[source,js] +-------------------------------------------------- +{ + "artifacts": [ + { + "type": "sourcemap", + "identifier": "foo-1.0.0", + "relative_url": "/api/fleet/artifacts/foo-1.0.0/644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", + "body": { + "serviceName": "foo", + "serviceVersion": "1.0.0", + "bundleFilepath": "/test/e2e/general-usecase/bundle.js.map", + "sourceMap": { + "version": 3, + "file": "static/js/main.chunk.js", + "sources": [ + "fleet-source-map-client/src/index.css", + "fleet-source-map-client/src/App.js", + "webpack:///./src/index.css?bb0a", + "fleet-source-map-client/src/index.js", + "fleet-source-map-client/src/reportWebVitals.js" + ], + "sourcesContent": [ + "content" + ], + "mappings": "mapping", + "sourceRoot": "" + } + }, + "created": "2021-07-09T20:47:44.812Z", + "id": "apm:foo-1.0.0-644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", + "compressionAlgorithm": "zlib", + "decodedSha256": "644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", + "decodedSize": 441, + "encodedSha256": "024c72749c3e3dd411b103f7040ae62633558608f480bce4b108cf5b2275bd24", + "encodedSize": 237, + "encryptionAlgorithm": "none", + "packageName": "apm" + } + ] +} +-------------------------------------------------- + +//// +******************************************************* +//// + +[[rum-sourcemap-delete]] +==== Delete source map + +Delete a previously uploaded source map. + +[[rum-sourcemap-delete-privs]] +===== Privileges + +The user accessing this endpoint requires `All` Kibana privileges for the {beat_kib_app} feature. +For more information, see <>. + +[[apm-sourcemap-delete-req]] +===== Request + +`DELETE /api/apm/sourcemaps/:id` + +[[apm-sourcemap-delete-example]] +===== Example + +The following example deletes a source map with an id of `apm:foo-1.0.0-644fd5a9`: + +[source,curl] +-------------------------------------------------- +curl -X DELETE "http://localhost:5601/api/apm/sourcemaps/apm:foo-1.0.0-644fd5a9" \ +-H 'Content-Type: application/json' \ +-H 'kbn-xsrf: true' \ +-H 'Authorization: ApiKey ${YOUR_API_KEY}' +-------------------------------------------------- + +[[apm-sourcemap-delete-body]] +===== Response body + +[source,js] +-------------------------------------------------- +{} +-------------------------------------------------- diff --git a/docs/apm/apm-app-users.asciidoc b/docs/apm/apm-app-users.asciidoc index 9b8a9c64ac43b..a8eb619a8eab8 100644 --- a/docs/apm/apm-app-users.asciidoc +++ b/docs/apm/apm-app-users.asciidoc @@ -3,7 +3,6 @@ == APM app users and privileges :beat_default_index_prefix: apm -:beat_kib_app: APM app :annotation_index: observability-annotations ++++ @@ -22,7 +21,7 @@ In general, there are three types of privileges you'll work with: * **Elasticsearch cluster privileges**: Manage the actions a user can perform against your cluster. * **Elasticsearch index privileges**: Control access to the data in specific indices your cluster. -* **Kibana space privileges**: Grant users write or read access to features and apps within Kibana. +* **Kibana feature privileges**: Grant users write or read access to features and apps within Kibana. Select your use-case to get started: @@ -88,18 +87,18 @@ include::./tab-widgets/apm-app-reader/widget.asciidoc[] TIP: Using the {apm-server-ref-v}/apm-integration.html[APM integration for Elastic Agent]? Add the privileges under the **Data streams** tab. -. Assign space privileges to any Kibana space that the user needs access to. +. Assign feature privileges to any Kibana feature that the user needs access to. Here are two examples: + [options="header"] |==== |Type | Privilege | Purpose -| Spaces -| `Read` or `All` on the {beat_kib_app} -| Allow the use of the the {beat_kib_app} +| Kibana +| `Read` or `All` on the {beat_kib_app} feature +| Allow the use of the the {beat_kib_app} apps -| Spaces +| Kibana | `Read` or `All` on Dashboards and Discover | Allow the user to view, edit, and create dashboards, as well as browse data. |==== @@ -190,16 +189,16 @@ include::./tab-widgets/central-config-users/widget.asciidoc[] TIP: Using the {apm-server-ref-v}/apm-integration.html[APM integration for Elastic Agent]? Add the privileges under the **Data streams** tab. -. Assign the `central-config-manager` role created in the previous step, and the following Kibana space privileges to -anyone who needs to manage central configurations: +. Assign the `central-config-manager` role created in the previous step, +and the following Kibana feature privileges to anyone who needs to manage central configurations: + [options="header"] |==== |Type | Privilege | Purpose -| Spaces -|`All` on {beat_kib_app} -|Allow full use of the {beat_kib_app} +| Kibana +|`All` on the {beat_kib_app} feature +|Allow full use of the {beat_kib_app} apps |==== [[apm-app-central-config-reader]] @@ -217,16 +216,16 @@ include::./tab-widgets/central-config-users/widget.asciidoc[] TIP: Using the {apm-server-ref-v}/apm-integration.html[APM integration for Elastic Agent]? Add the privileges under the **Data streams** tab. -. Assign the `central-config-reader` role created in the previous step, and the following Kibana space privileges to -anyone who needs to read central configurations: +. Assign the `central-config-reader` role created in the previous step, +and the following Kibana feature privileges to anyone who needs to read central configurations: + [options="header"] |==== |Type | Privilege | Purpose -| Spaces -|`read` on the {beat_kib_app} -|Allow read access to the {beat_kib_app} +| Kibana +|`read` on the {beat_kib_app} feature +|Allow read access to the {beat_kib_app} apps |==== [[apm-app-central-config-api]] @@ -253,15 +252,15 @@ include::./tab-widgets/code.asciidoc[] Users can list, search, create, update, and delete central configurations via the APM app API. -. Assign the following Kibana space privileges: +. Assign the following Kibana feature privileges: + [options="header"] |==== |Type | Privilege | Purpose -| Spaces -|`all` on the {beat_kib_app} -|Allow all access to the {beat_kib_app} +| Kibana +|`all` on the {beat_kib_app} feature +|Allow all access to the {beat_kib_app} apps |==== [[apm-app-api-config-reader]] @@ -269,15 +268,15 @@ Users can list, search, create, update, and delete central configurations via th Sometimes a user only needs to list and search central configurations via the APM app API. -. Assign the following Kibana space privileges: +. Assign the following Kibana feature privileges: + [options="header"] |==== |Type | Privilege | Purpose -| Spaces -|`read` on the {beat_kib_app} -|Allow read access to the {beat_kib_app} +| Kibana +|`read` on the {beat_kib_app} feature +|Allow read access to the {beat_kib_app} apps |==== [[apm-app-api-annotation-manager]] @@ -310,15 +309,15 @@ and assign the following privileges: |==== . Assign the `annotation_role` created previously, -and the following Kibana space privileges to any annotation API users: +and the following Kibana feature privileges to any annotation API users: + [options="header"] |==== |Type | Privilege | Purpose -| Spaces -|`all` on the {beat_kib_app} -|Allow all access to the {beat_kib_app} +| Kibana +|`all` on the {beat_kib_app} feature +|Allow all access to the {beat_kib_app} apps |==== //LEARN MORE diff --git a/docs/apm/index.asciidoc b/docs/apm/index.asciidoc index 53ffee5e061d6..f4d35a2d554ba 100644 --- a/docs/apm/index.asciidoc +++ b/docs/apm/index.asciidoc @@ -25,6 +25,8 @@ you can also see contextual information such as the request header, user informa system values, or custom data that you manually attached to the request. -- +:beat_kib_app: APM and User Experience + include::set-up.asciidoc[] include::getting-started.asciidoc[] diff --git a/docs/concepts/images/add-filter-popup.png b/docs/concepts/images/add-filter-popup.png index f96c8746ef17a..28a2dc0013f60 100644 Binary files a/docs/concepts/images/add-filter-popup.png and b/docs/concepts/images/add-filter-popup.png differ diff --git a/docs/concepts/images/global-search.png b/docs/concepts/images/global-search.png index a867477938219..c5080da40d97e 100644 Binary files a/docs/concepts/images/global-search.png and b/docs/concepts/images/global-search.png differ diff --git a/docs/concepts/images/refresh-every.png b/docs/concepts/images/refresh-every.png index ea3b24d80b8c3..6b9ff7c8f9d9f 100644 Binary files a/docs/concepts/images/refresh-every.png and b/docs/concepts/images/refresh-every.png differ diff --git a/docs/concepts/images/saved-query-popup.png b/docs/concepts/images/saved-query-popup.png index 93973b8de0a54..a139cab486257 100644 Binary files a/docs/concepts/images/saved-query-popup.png and b/docs/concepts/images/saved-query-popup.png differ diff --git a/docs/concepts/images/saved-query.png b/docs/concepts/images/saved-query.png index 5db819ce648e8..eb0c0f984f483 100644 Binary files a/docs/concepts/images/saved-query.png and b/docs/concepts/images/saved-query.png differ diff --git a/docs/concepts/images/time-filter.png b/docs/concepts/images/time-filter.png index e3a1ce2216c1c..7bf7839ec773d 100644 Binary files a/docs/concepts/images/time-filter.png and b/docs/concepts/images/time-filter.png differ diff --git a/docs/concepts/images/time-relative.png b/docs/concepts/images/time-relative.png index b77d55df2d9ea..462687de720e0 100644 Binary files a/docs/concepts/images/time-relative.png and b/docs/concepts/images/time-relative.png differ diff --git a/docs/concepts/index.asciidoc b/docs/concepts/index.asciidoc index 43e5ae733a760..ae6e48556f684 100644 --- a/docs/concepts/index.asciidoc +++ b/docs/concepts/index.asciidoc @@ -83,7 +83,7 @@ resubmit your searches. You can also click *Refresh* to resubmit the search. This might be useful if you use {kib} to monitor the underlying data. [role="screenshot"] -image:concepts/images/refresh-every.png["section of time filter where you can configure a refresh rate"] +image:concepts/images/refresh-every.png["section of time filter where you can configure a refresh rate", width=75%] [float] diff --git a/docs/concepts/kuery.asciidoc b/docs/concepts/kuery.asciidoc index a92fc182f388c..53d445b932d62 100644 --- a/docs/concepts/kuery.asciidoc +++ b/docs/concepts/kuery.asciidoc @@ -6,10 +6,7 @@ free text search or field-based search. KQL is only used for filtering data, and no role in sorting or aggregating the data. KQL is able to suggest field names, values, and operators as you type. -The performance of the suggestions is controlled by <>: - -[role="screenshot"] -image::images/kql-autocomplete.png[Autocomplete in Search bar] +The performance of the suggestions is controlled by <>. KQL has a different set of features than the <>. KQL is able to query nested fields and <>. KQL does not support regular expressions diff --git a/docs/concepts/save-query.asciidoc b/docs/concepts/save-query.asciidoc index fa626f6eaa913..61113b5491c29 100644 --- a/docs/concepts/save-query.asciidoc +++ b/docs/concepts/save-query.asciidoc @@ -35,7 +35,7 @@ For more information, see <>. * *Refresh every* to specify an automatic refresh rate. + [role="screenshot"] -image::concepts/images/time-filter.png[Time filter menu] +image::concepts/images/time-filter.png["Time filter menu", width=75%] . To set start and end times, click the bar next to the time filter. In the popup, select *Absolute*, *Relative* or *Now*, then specify the required options. + [role="screenshot"] -image::concepts/images/time-relative.png[Time filter showing relative time] +image::concepts/images/time-relative.png["Time filter showing relative time", width=75%] diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md index 413b4aaf46b50..96ca86cffbc5f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md @@ -16,4 +16,5 @@ export interface IExecutionContextContainer | Property | Type | Description | | --- | --- | --- | | [toHeader](./kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md) | () => Record<string, string> | | +| [toJSON](./kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md) | () => Readonly<KibanaExecutionContext> | | diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md new file mode 100644 index 0000000000000..916148141c8f3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) > [toJSON](./kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md) + +## IExecutionContextContainer.toJSON property + +Signature: + +```typescript +toJSON: () => Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md index 1b50d29094585..d17f9cb8a7ff3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md +++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md @@ -4,7 +4,7 @@ ## KibanaExecutionContext.id property -unique value to indentify find the source +unique value to identify the source Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md index 41724f4914264..afd66fbba18d3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md +++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md @@ -16,7 +16,7 @@ export interface KibanaExecutionContext | Property | Type | Description | | --- | --- | --- | | [description](./kibana-plugin-core-public.kibanaexecutioncontext.description.md) | string | human readable description. For example, a vis title, action name | -| [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md) | string | unique value to indentify find the source | +| [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md) | string | unique value to identify the source | | [name](./kibana-plugin-core-public.kibanaexecutioncontext.name.md) | string | public name of a user-facing feature | | [type](./kibana-plugin-core-public.kibanaexecutioncontext.type.md) | string | Kibana application initated an operation. Can be narrowed to an enum later. | | [url](./kibana-plugin-core-public.kibanaexecutioncontext.url.md) | string | in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url | diff --git a/docs/development/core/server/kibana-plugin-core-server.corepreboot.elasticsearch.md b/docs/development/core/server/kibana-plugin-core-server.corepreboot.elasticsearch.md new file mode 100644 index 0000000000000..7d3b5296b5988 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corepreboot.elasticsearch.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CorePreboot](./kibana-plugin-core-server.corepreboot.md) > [elasticsearch](./kibana-plugin-core-server.corepreboot.elasticsearch.md) + +## CorePreboot.elasticsearch property + +[ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) + +Signature: + +```typescript +elasticsearch: ElasticsearchServicePreboot; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corepreboot.http.md b/docs/development/core/server/kibana-plugin-core-server.corepreboot.http.md new file mode 100644 index 0000000000000..0df643c6f133b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corepreboot.http.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CorePreboot](./kibana-plugin-core-server.corepreboot.md) > [http](./kibana-plugin-core-server.corepreboot.http.md) + +## CorePreboot.http property + +[HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) + +Signature: + +```typescript +http: HttpServicePreboot; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corepreboot.md b/docs/development/core/server/kibana-plugin-core-server.corepreboot.md new file mode 100644 index 0000000000000..475b5f109d58c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corepreboot.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CorePreboot](./kibana-plugin-core-server.corepreboot.md) + +## CorePreboot interface + +Context passed to the `setup` method of `preboot` plugins. + +Signature: + +```typescript +export interface CorePreboot +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [elasticsearch](./kibana-plugin-core-server.corepreboot.elasticsearch.md) | ElasticsearchServicePreboot | [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) | +| [http](./kibana-plugin-core-server.corepreboot.http.md) | HttpServicePreboot | [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) | +| [preboot](./kibana-plugin-core-server.corepreboot.preboot.md) | PrebootServicePreboot | [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) | + diff --git a/docs/development/core/server/kibana-plugin-core-server.corepreboot.preboot.md b/docs/development/core/server/kibana-plugin-core-server.corepreboot.preboot.md new file mode 100644 index 0000000000000..3780a92053a5e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corepreboot.preboot.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CorePreboot](./kibana-plugin-core-server.corepreboot.md) > [preboot](./kibana-plugin-core-server.corepreboot.preboot.md) + +## CorePreboot.preboot property + +[PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) + +Signature: + +```typescript +preboot: PrebootServicePreboot; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index a66db46adf0f7..b03101b4d9fe6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -4,7 +4,7 @@ ## CoreSetup interface -Context passed to the plugins `setup` method. +Context passed to the `setup` method of `standard` plugins. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md index b88a179c5c4b3..042f2d1485618 100644 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md +++ b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md @@ -21,4 +21,5 @@ export interface DiscoveredPlugin | [optionalPlugins](./kibana-plugin-core-server.discoveredplugin.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | | [requiredBundles](./kibana-plugin-core-server.discoveredplugin.requiredbundles.md) | readonly PluginName[] | List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins. | | [requiredPlugins](./kibana-plugin-core-server.discoveredplugin.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | +| [type](./kibana-plugin-core-server.discoveredplugin.type.md) | PluginType | Type of the plugin, defaults to standard. | diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.type.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.type.md new file mode 100644 index 0000000000000..0a33be0d63f5c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [type](./kibana-plugin-core-server.discoveredplugin.type.md) + +## DiscoveredPlugin.type property + +Type of the plugin, defaults to `standard`. + +Signature: + +```typescript +readonly type: PluginType; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.credentialsspecified.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.credentialsspecified.md new file mode 100644 index 0000000000000..df99d5ec4b831 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.credentialsspecified.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchConfigPreboot](./kibana-plugin-core-server.elasticsearchconfigpreboot.md) > [credentialsSpecified](./kibana-plugin-core-server.elasticsearchconfigpreboot.credentialsspecified.md) + +## ElasticsearchConfigPreboot.credentialsSpecified property + +Indicates whether Elasticsearch configuration includes credentials (`username`, `password` or `serviceAccountToken`). + +Signature: + +```typescript +readonly credentialsSpecified: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.hosts.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.hosts.md new file mode 100644 index 0000000000000..e9ad47b61419e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.hosts.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchConfigPreboot](./kibana-plugin-core-server.elasticsearchconfigpreboot.md) > [hosts](./kibana-plugin-core-server.elasticsearchconfigpreboot.hosts.md) + +## ElasticsearchConfigPreboot.hosts property + +Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. + +Signature: + +```typescript +readonly hosts: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.md new file mode 100644 index 0000000000000..bbccea80b672f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchConfigPreboot](./kibana-plugin-core-server.elasticsearchconfigpreboot.md) + +## ElasticsearchConfigPreboot interface + +A limited set of Elasticsearch configuration entries exposed to the `preboot` plugins at `setup`. + +Signature: + +```typescript +export interface ElasticsearchConfigPreboot +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [credentialsSpecified](./kibana-plugin-core-server.elasticsearchconfigpreboot.credentialsspecified.md) | boolean | Indicates whether Elasticsearch configuration includes credentials (username, password or serviceAccountToken). | +| [hosts](./kibana-plugin-core-server.elasticsearchconfigpreboot.hosts.md) | string[] | Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.config.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.config.md new file mode 100644 index 0000000000000..12a32b4544aba --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.config.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) > [config](./kibana-plugin-core-server.elasticsearchservicepreboot.config.md) + +## ElasticsearchServicePreboot.config property + +A limited set of Elasticsearch configuration entries. + +Signature: + +```typescript +readonly config: Readonly; +``` + +## Example + + +```js +const { hosts, credentialsSpecified } = core.elasticsearch.config; + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md new file mode 100644 index 0000000000000..d14e3e4efa400 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) > [createClient](./kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md) + +## ElasticsearchServicePreboot.createClient property + +Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). + +Signature: + +```typescript +readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; +``` + +## Example + + +```js +const client = elasticsearch.createClient('my-app-name', config); +const data = await client.asInternalUser.search(); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.md new file mode 100644 index 0000000000000..bf458004b488b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) + +## ElasticsearchServicePreboot interface + + +Signature: + +```typescript +export interface ElasticsearchServicePreboot +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [config](./kibana-plugin-core-server.elasticsearchservicepreboot.config.md) | Readonly<ElasticsearchConfigPreboot> | A limited set of Elasticsearch configuration entries. | +| [createClient](./kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). | + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.basepath.md b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.basepath.md new file mode 100644 index 0000000000000..9864f67d70a43 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.basepath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) > [basePath](./kibana-plugin-core-server.httpservicepreboot.basepath.md) + +## HttpServicePreboot.basePath property + +Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). + +Signature: + +```typescript +basePath: IBasePath; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.md b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.md new file mode 100644 index 0000000000000..b4adf454a480f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.md @@ -0,0 +1,82 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) + +## HttpServicePreboot interface + +Kibana HTTP Service provides an abstraction to work with the HTTP stack at the `preboot` stage. This functionality allows Kibana to serve user requests even before Kibana becomes fully operational. Only Core and `preboot` plugins can define HTTP routes at this stage. + +Signature: + +```typescript +export interface HttpServicePreboot +``` + +## Example + +To handle an incoming request in your preboot plugin you should: - Use `@kbn/config-schema` package to create a schema to validate the request `params`, `query`, and `body`. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error without calling the route's handler. To opt out of validating the request, specify `false`. + +```ts +import { schema, TypeOf } from '@kbn/config-schema'; +const validate = { + params: schema.object({ + id: schema.string(), + }), +}; + +``` +- Declare a function to respond to incoming request. The function will receive `request` object containing request details: url, headers, matched route, as well as validated `params`, `query`, `body`. And `response` object instructing HTTP server to create HTTP response with information sent back to the client as the response body, headers, and HTTP status. Any exception raised during the handler call will generate `500 Server error` response and log error details for further investigation. See below for returning custom error responses. + +```ts +const handler = async (context: RequestHandlerContext, request: KibanaRequest, response: ResponseFactory) => { + const data = await findObject(request.params.id); + // creates a command to respond with 'not found' error + if (!data) { + return response.notFound(); + } + // creates a command to send found data to the client and set response headers + return response.ok({ + body: data, + headers: { 'content-type': 'application/json' } + }); +} + +``` +\* - Acquire `preboot` [IRouter](./kibana-plugin-core-server.irouter.md) instance and register route handler for GET request to 'path/{id}' path. + +```ts +import { schema, TypeOf } from '@kbn/config-schema'; + +const validate = { + params: schema.object({ + id: schema.string(), + }), +}; + +httpPreboot.registerRoutes('my-plugin', (router) => { + router.get({ path: 'path/{id}', validate }, async (context, request, response) => { + const data = await findObject(request.params.id); + if (!data) { + return response.notFound(); + } + return response.ok({ + body: data, + headers: { 'content-type': 'application/json' } + }); + }); +}); + +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [basePath](./kibana-plugin-core-server.httpservicepreboot.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). | + +## Methods + +| Method | Description | +| --- | --- | +| [registerRoutes(path, callback)](./kibana-plugin-core-server.httpservicepreboot.registerroutes.md) | Provides ability to acquire preboot [IRouter](./kibana-plugin-core-server.irouter.md) instance for a particular top-level path and register handler functions for any number of nested routes. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.registerroutes.md b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.registerroutes.md new file mode 100644 index 0000000000000..c188f0ba0ce94 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.registerroutes.md @@ -0,0 +1,40 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) > [registerRoutes](./kibana-plugin-core-server.httpservicepreboot.registerroutes.md) + +## HttpServicePreboot.registerRoutes() method + +Provides ability to acquire `preboot` [IRouter](./kibana-plugin-core-server.irouter.md) instance for a particular top-level path and register handler functions for any number of nested routes. + +Signature: + +```typescript +registerRoutes(path: string, callback: (router: IRouter) => void): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| path | string | | +| callback | (router: IRouter) => void | | + +Returns: + +`void` + +## Remarks + +Each route can have only one handler function, which is executed when the route is matched. See the [IRouter](./kibana-plugin-core-server.irouter.md) documentation for more information. + +## Example + + +```ts +registerRoutes('my-plugin', (router) => { + // handler is called when '/my-plugin/path' resource is requested with `GET` method + router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); +}); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md index d86f621231214..4ade96691fb14 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md @@ -4,7 +4,7 @@ ## KibanaExecutionContext.id property -unique value to indentify find the source +unique value to identify the source Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md index ebc2aeb419a75..517e8f3ffda1f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md @@ -16,7 +16,7 @@ export interface KibanaExecutionContext | Property | Type | Description | | --- | --- | --- | | [description](./kibana-plugin-core-server.kibanaexecutioncontext.description.md) | string | human readable description. For example, a vis title, action name | -| [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md) | string | unique value to indentify find the source | +| [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md) | string | unique value to identify the source | | [name](./kibana-plugin-core-server.kibanaexecutioncontext.name.md) | string | public name of a user-facing feature | | [type](./kibana-plugin-core-server.kibanaexecutioncontext.type.md) | string | Kibana application initated an operation. Can be narrowed to an enum later. | | [url](./kibana-plugin-core-server.kibanaexecutioncontext.url.md) | string | in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 4a203f10e7cd3..a26f8bd7b1594 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -41,6 +41,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | --- | --- | | [AuthResultType](./kibana-plugin-core-server.authresulttype.md) | | | [AuthStatus](./kibana-plugin-core-server.authstatus.md) | Status indicating an outcome of the authentication. | +| [PluginType](./kibana-plugin-core-server.plugintype.md) | | ## Interfaces @@ -60,7 +61,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) | APIs to manage the [Capabilities](./kibana-plugin-core-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-core-server.capabilitiesstart.md) | APIs to access the application [Capabilities](./kibana-plugin-core-server.capabilities.md). | | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | -| [CoreSetup](./kibana-plugin-core-server.coresetup.md) | Context passed to the plugins setup method. | +| [CorePreboot](./kibana-plugin-core-server.corepreboot.md) | Context passed to the setup method of preboot plugins. | +| [CoreSetup](./kibana-plugin-core-server.coresetup.md) | Context passed to the setup method of standard plugins. | | [CoreStart](./kibana-plugin-core-server.corestart.md) | Context passed to the plugins start method. | | [CoreStatus](./kibana-plugin-core-server.corestatus.md) | Status of core services. | | [CountResponse](./kibana-plugin-core-server.countresponse.md) | | @@ -73,6 +75,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [DeprecationSettings](./kibana-plugin-core-server.deprecationsettings.md) | UiSettings deprecation field options. | | [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) | The deprecations service provides a way for the Kibana platform to communicate deprecated features and configs with its users. These deprecations are only communicated if the deployment is using these features. Allowing for a user tailored experience for upgrading the stack version.The Deprecation service is consumed by the upgrade assistant to assist with the upgrade experience.If a deprecated feature can be resolved without manual user intervention. Using correctiveActions.api allows the Upgrade Assistant to use this api to correct the deprecation upon a user trigger. | | [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | +| [ElasticsearchConfigPreboot](./kibana-plugin-core-server.elasticsearchconfigpreboot.md) | A limited set of Elasticsearch configuration entries exposed to the preboot plugins at setup. | +| [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) | | | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | | [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) | | @@ -87,6 +91,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) | Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. | | [HttpResponseOptions](./kibana-plugin-core-server.httpresponseoptions.md) | HTTP response parameters | | [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | Information about what hostname, port, and protocol the server process is running on. Note that this may not match the URL that end-users access Kibana at. For the public URL, see [BasePath.publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md). | +| [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) | Kibana HTTP Service provides an abstraction to work with the HTTP stack at the preboot stage. This functionality allows Kibana to serve user requests even before Kibana becomes fully operational. Only Core and preboot plugins can define HTTP routes at this stage. | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | | | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | | @@ -128,10 +133,12 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) | OS related metrics | | [OpsProcessMetrics](./kibana-plugin-core-server.opsprocessmetrics.md) | Process related metrics | | [OpsServerMetrics](./kibana-plugin-core-server.opsservermetrics.md) | server related metrics | -| [Plugin](./kibana-plugin-core-server.plugin.md) | The interface that should be returned by a PluginInitializer. | +| [Plugin](./kibana-plugin-core-server.plugin.md) | The interface that should be returned by a PluginInitializer for a standard plugin. | | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | +| [PrebootPlugin](./kibana-plugin-core-server.prebootplugin.md) | The interface that should be returned by a PluginInitializer for a preboot plugin. | +| [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) | Kibana Preboot Service allows to control the boot flow of Kibana. Preboot plugins can use it to hold the boot until certain condition is met. | | [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) | | | [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [ResolveCapabilitiesOptions](./kibana-plugin-core-server.resolvecapabilitiesoptions.md) | Defines a set of additional options for the resolveCapabilities method of [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md). | diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.md b/docs/development/core/server/kibana-plugin-core-server.plugin.md index d9796202d7878..b1fce06d46f3a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.md @@ -4,7 +4,7 @@ ## Plugin interface -The interface that should be returned by a `PluginInitializer`. +The interface that should be returned by a `PluginInitializer` for a `standard` plugin. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md index fe55e131065dd..9b4d1b022db2a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `server` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | PrebootPlugin | AsyncPlugin; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.env.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.env.md index 76e4f222f0228..534f532850587 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.env.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.env.md @@ -11,5 +11,6 @@ env: { mode: EnvironmentMode; packageInfo: Readonly; instanceUuid: string; + configs: readonly string[]; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md index 90a19d53bd5e1..9bc9d6d83674c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md @@ -17,7 +17,7 @@ export interface PluginInitializerContext | Property | Type | Description | | --- | --- | --- | | [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
get: () => SharedGlobalConfig;
};
create: <T = ConfigSchema>() => Observable<T>;
get: <T = ConfigSchema>() => T;
} | Accessors for the plugin's configuration | -| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
instanceUuid: string;
} | | +| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
instanceUuid: string;
configs: readonly string[];
} | | | [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | LoggerFactory | instance already bound to the plugin's logging context | | [opaqueId](./kibana-plugin-core-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId | | diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md index b3e20bc7ed693..f8d4c3f1b9d15 100644 --- a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md @@ -31,6 +31,7 @@ Should never be used in code outside of Core but is exported for documentation p | [requiredPlugins](./kibana-plugin-core-server.pluginmanifest.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | | [server](./kibana-plugin-core-server.pluginmanifest.server.md) | boolean | Specifies whether plugin includes some server-side specific functionality. | | [serviceFolders](./kibana-plugin-core-server.pluginmanifest.servicefolders.md) | readonly string[] | Only used for the automatically generated API documentation. Specifying service folders will cause your plugin API reference to be broken up into sub sections. | +| [type](./kibana-plugin-core-server.pluginmanifest.type.md) | PluginType | Type of the plugin, defaults to standard. | | [ui](./kibana-plugin-core-server.pluginmanifest.ui.md) | boolean | Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via public/ui_plugin.js file. | | [version](./kibana-plugin-core-server.pluginmanifest.version.md) | string | Version of the plugin. | diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.type.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.type.md new file mode 100644 index 0000000000000..6e82132919f6d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) > [type](./kibana-plugin-core-server.pluginmanifest.type.md) + +## PluginManifest.type property + +Type of the plugin, defaults to `standard`. + +Signature: + +```typescript +readonly type: PluginType; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugintype.md b/docs/development/core/server/kibana-plugin-core-server.plugintype.md new file mode 100644 index 0000000000000..e4a252a392949 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.plugintype.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginType](./kibana-plugin-core-server.plugintype.md) + +## PluginType enum + + +Signature: + +```typescript +export declare enum PluginType +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| preboot | "preboot" | Preboot plugins are special-purpose plugins that only function during preboot stage. | +| standard | "standard" | Standard plugins are plugins that start to function as soon as Kibana is fully booted and are active until it shuts down. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootplugin.md b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.md new file mode 100644 index 0000000000000..df851daab7806 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PrebootPlugin](./kibana-plugin-core-server.prebootplugin.md) + +## PrebootPlugin interface + +The interface that should be returned by a `PluginInitializer` for a `preboot` plugin. + +Signature: + +```typescript +export interface PrebootPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-server.prebootplugin.setup.md) | | +| [stop()](./kibana-plugin-core-server.prebootplugin.stop.md) | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootplugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.setup.md new file mode 100644 index 0000000000000..0ee2a26293e98 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PrebootPlugin](./kibana-plugin-core-server.prebootplugin.md) > [setup](./kibana-plugin-core-server.prebootplugin.setup.md) + +## PrebootPlugin.setup() method + +Signature: + +```typescript +setup(core: CorePreboot, plugins: TPluginsSetup): TSetup; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CorePreboot | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup` + diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootplugin.stop.md b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.stop.md new file mode 100644 index 0000000000000..89566b2ae6687 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PrebootPlugin](./kibana-plugin-core-server.prebootplugin.md) > [stop](./kibana-plugin-core-server.prebootplugin.stop.md) + +## PrebootPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.holdsetupuntilresolved.md b/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.holdsetupuntilresolved.md new file mode 100644 index 0000000000000..7f158b46d1f06 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.holdsetupuntilresolved.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) > [holdSetupUntilResolved](./kibana-plugin-core-server.prebootservicepreboot.holdsetupuntilresolved.md) + +## PrebootServicePreboot.holdSetupUntilResolved property + +Registers a `Promise` as a precondition before Kibana can proceed to `setup`. This method can be invoked multiple times and from multiple `preboot` plugins. Kibana will proceed to `setup` only when all registered `Promises` instances are resolved, or it will shut down if any of them is rejected. + +Signature: + +```typescript +readonly holdSetupUntilResolved: (reason: string, promise: Promise<{ + shouldReloadConfig: boolean; + } | undefined>) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.issetuponhold.md b/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.issetuponhold.md new file mode 100644 index 0000000000000..1ba079da03208 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.issetuponhold.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) > [isSetupOnHold](./kibana-plugin-core-server.prebootservicepreboot.issetuponhold.md) + +## PrebootServicePreboot.isSetupOnHold property + +Indicates whether Kibana is currently on hold and cannot proceed to `setup` yet. + +Signature: + +```typescript +readonly isSetupOnHold: () => boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.md b/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.md new file mode 100644 index 0000000000000..bf503499b6298 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.md @@ -0,0 +1,45 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) + +## PrebootServicePreboot interface + +Kibana Preboot Service allows to control the boot flow of Kibana. Preboot plugins can use it to hold the boot until certain condition is met. + +Signature: + +```typescript +export interface PrebootServicePreboot +``` + +## Example + +A plugin can supply a `Promise` to a `holdSetupUntilResolved` method to signal Kibana to initialize and start `standard` plugins only after this `Promise` is resolved. If `Promise` is rejected, Kibana will shut down. + +```ts +core.preboot.holdSetupUntilResolved('Just waiting for 5 seconds', + new Promise((resolve) => { + setTimeout(resolve, 5000); + }) +); + +``` +If the supplied `Promise` resolves to an object with the `shouldReloadConfig` property set to `true`, Kibana will also reload its configuration from disk. + +```ts +let completeSetup: (result: { shouldReloadConfig: boolean }) => void; +core.preboot.holdSetupUntilResolved('Just waiting for 5 seconds before reloading configuration', + new Promise<{ shouldReloadConfig: boolean }>((resolve) => { + setTimeout(() => resolve({ shouldReloadConfig: true }), 5000); + }) +); + +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [holdSetupUntilResolved](./kibana-plugin-core-server.prebootservicepreboot.holdsetupuntilresolved.md) | (reason: string, promise: Promise<{
shouldReloadConfig: boolean;
} | undefined>) => void | Registers a Promise as a precondition before Kibana can proceed to setup. This method can be invoked multiple times and from multiple preboot plugins. Kibana will proceed to setup only when all registered Promises instances are resolved, or it will shut down if any of them is rejected. | +| [isSetupOnHold](./kibana-plugin-core-server.prebootservicepreboot.issetuponhold.md) | () => boolean | Indicates whether Kibana is currently on hold and cannot proceed to setup yet. | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.executioncontext.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.executioncontext.md new file mode 100644 index 0000000000000..18fce3e273a39 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.executioncontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [executionContext](./kibana-plugin-plugins-data-public.isearchoptions.executioncontext.md) + +## ISearchOptions.executionContext property + +Signature: + +```typescript +executionContext?: KibanaExecutionContext; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md index 21fb7e3dfc7e8..488695475dcbe 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md @@ -15,6 +15,7 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [executionContext](./kibana-plugin-plugins-data-public.isearchoptions.executioncontext.md) | KibanaExecutionContext | | | [indexPattern](./kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md) | IndexPattern | Index pattern reference is used for better error messages | | [inspector](./kibana-plugin-plugins-data-public.isearchoptions.inspector.md) | IInspectorInfo | Inspector integration options | | [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.executioncontext.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.executioncontext.md new file mode 100644 index 0000000000000..72e750e9be92a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.executioncontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [executionContext](./kibana-plugin-plugins-data-server.isearchoptions.executioncontext.md) + +## ISearchOptions.executionContext property + +Signature: + +```typescript +executionContext?: KibanaExecutionContext; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index cdb5664f96cdd..674cacc27a7e4 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -15,6 +15,7 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [executionContext](./kibana-plugin-plugins-data-server.isearchoptions.executioncontext.md) | KibanaExecutionContext | | | [indexPattern](./kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md) | IndexPattern | Index pattern reference is used for better error messages | | [inspector](./kibana-plugin-plugins-data-server.isearchoptions.inspector.md) | IInspectorInfo | Inspector integration options | | [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md new file mode 100644 index 0000000000000..bfe1925a21f68 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) > [getExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md) + +## ExecutionContext.getExecutionContext property + +Contains the meta-data about the source of the expression. + +Signature: + +```typescript +getExecutionContext: () => IExecutionContextContainer | undefined; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md index 1388e04c315e2..38165a1683316 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md @@ -17,6 +17,7 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | +| [getExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md) | () => IExecutionContextContainer | undefined | Contains the meta-data about the source of the expression. | | [getKibanaRequest](./kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md) | () => KibanaRequest | Getter to retrieve the KibanaRequest object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | | [getSearchContext](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md new file mode 100644 index 0000000000000..504f5e2df7d6e --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [executionContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md) + +## IExpressionLoaderParams.executionContext property + +Signature: + +```typescript +executionContext?: IExecutionContextContainer; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index 1bee155250252..dcdbd663f84e4 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -19,6 +19,7 @@ export interface IExpressionLoaderParams | [customRenderers](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customrenderers.md) | [] | | | [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) | boolean | | | [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | boolean | | +| [executionContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md) | IExecutionContextContainer | | | [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | ExpressionRenderHandlerParams['hasCompatibleActions'] | | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md new file mode 100644 index 0000000000000..b4ceb8f96d698 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) > [getExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md) + +## ExecutionContext.getExecutionContext property + +Contains the meta-data about the source of the expression. + +Signature: + +```typescript +getExecutionContext: () => IExecutionContextContainer | undefined; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md index 8503f81ad7d25..3b308ca46ab0a 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md @@ -17,6 +17,7 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | +| [getExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md) | () => IExecutionContextContainer | undefined | Contains the meta-data about the source of the expression. | | [getKibanaRequest](./kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md) | () => KibanaRequest | Getter to retrieve the KibanaRequest object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | | [getSearchContext](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | diff --git a/docs/discover/images/add-field-to-pattern.png b/docs/discover/images/add-field-to-pattern.png index 04f564ab89857..9a206f5f1bd1d 100644 Binary files a/docs/discover/images/add-field-to-pattern.png and b/docs/discover/images/add-field-to-pattern.png differ diff --git a/docs/discover/images/customer.png b/docs/discover/images/customer.png index f2042080f0a37..4c1ff2f2fddbd 100644 Binary files a/docs/discover/images/customer.png and b/docs/discover/images/customer.png differ diff --git a/docs/discover/images/discover-add-filter.png b/docs/discover/images/discover-add-filter.png index 4a5f6d1a9fa99..5ee2f01960248 100644 Binary files a/docs/discover/images/discover-add-filter.png and b/docs/discover/images/discover-add-filter.png differ diff --git a/docs/discover/images/discover-context-filters-inactive.png b/docs/discover/images/discover-context-filters-inactive.png index c53a8cedff336..26609392472fb 100644 Binary files a/docs/discover/images/discover-context-filters-inactive.png and b/docs/discover/images/discover-context-filters-inactive.png differ diff --git a/docs/discover/images/discover-context-load-newer-documents.png b/docs/discover/images/discover-context-load-newer-documents.png index 65d168f5ca4af..237dc4eb2683f 100644 Binary files a/docs/discover/images/discover-context-load-newer-documents.png and b/docs/discover/images/discover-context-load-newer-documents.png differ diff --git a/docs/discover/images/discover-context.png b/docs/discover/images/discover-context.png index 8ce68047e0d76..f73cba203f8f1 100644 Binary files a/docs/discover/images/discover-context.png and b/docs/discover/images/discover-context.png differ diff --git a/docs/discover/images/discover-from-visualize.png b/docs/discover/images/discover-from-visualize.png index 07d34b8b49018..42d46e6cbd5b5 100644 Binary files a/docs/discover/images/discover-from-visualize.png and b/docs/discover/images/discover-from-visualize.png differ diff --git a/docs/discover/images/discover-index-pattern.png b/docs/discover/images/discover-index-pattern.png index b9f96f1f0cedd..a7ff9bf834e40 100644 Binary files a/docs/discover/images/discover-index-pattern.png and b/docs/discover/images/discover-index-pattern.png differ diff --git a/docs/discover/images/discover-save-saved-search.png b/docs/discover/images/discover-save-saved-search.png index bf22408729b08..bd13f800b7f33 100644 Binary files a/docs/discover/images/discover-save-saved-search.png and b/docs/discover/images/discover-save-saved-search.png differ diff --git a/docs/discover/images/discover-search-field.png b/docs/discover/images/discover-search-field.png index 9acbf86067fb4..f04abfa566fce 100644 Binary files a/docs/discover/images/discover-search-field.png and b/docs/discover/images/discover-search-field.png differ diff --git a/docs/discover/images/discover-search-for-relevance.png b/docs/discover/images/discover-search-for-relevance.png index e42ed2416f79e..64cfd87b7aac2 100644 Binary files a/docs/discover/images/discover-search-for-relevance.png and b/docs/discover/images/discover-search-for-relevance.png differ diff --git a/docs/discover/images/discover-sidebar-available-fields.png b/docs/discover/images/discover-sidebar-available-fields.png index b34119ca4a039..e330036524ae7 100644 Binary files a/docs/discover/images/discover-sidebar-available-fields.png and b/docs/discover/images/discover-sidebar-available-fields.png differ diff --git a/docs/discover/images/discover-view-single-document.png b/docs/discover/images/discover-view-single-document.png index d803acc49ce24..18ff5f67eb5d8 100644 Binary files a/docs/discover/images/discover-view-single-document.png and b/docs/discover/images/discover-view-single-document.png differ diff --git a/docs/discover/images/discover-visualize.png b/docs/discover/images/discover-visualize.png index 242aa16709a9f..5aca5a210139d 100644 Binary files a/docs/discover/images/discover-visualize.png and b/docs/discover/images/discover-visualize.png differ diff --git a/docs/discover/images/document-table-expanded.png b/docs/discover/images/document-table-expanded.png index 38ea0987b120a..ebbd2e607eb5a 100644 Binary files a/docs/discover/images/document-table-expanded.png and b/docs/discover/images/document-table-expanded.png differ diff --git a/docs/discover/images/document-table.png b/docs/discover/images/document-table.png index 097feb4dc9034..5b5dbc08d6e64 100644 Binary files a/docs/discover/images/document-table.png and b/docs/discover/images/document-table.png differ diff --git a/docs/discover/images/find-manufacturer-field.png b/docs/discover/images/find-manufacturer-field.png index f02672f531edf..ce78aab933362 100644 Binary files a/docs/discover/images/find-manufacturer-field.png and b/docs/discover/images/find-manufacturer-field.png differ diff --git a/docs/discover/images/hello-field.png b/docs/discover/images/hello-field.png index fe4cfbb41fdc2..5c6348d4e90fe 100644 Binary files a/docs/discover/images/hello-field.png and b/docs/discover/images/hello-field.png differ diff --git a/docs/discover/images/kql-autocomplete.png b/docs/discover/images/kql-autocomplete.png index 60e2290c0bad7..599ff2aa2e65a 100644 Binary files a/docs/discover/images/kql-autocomplete.png and b/docs/discover/images/kql-autocomplete.png differ diff --git a/docs/discover/images/saved-search.png b/docs/discover/images/saved-search.png index cf0a89fe1a79f..10bc53d37785b 100644 Binary files a/docs/discover/images/saved-search.png and b/docs/discover/images/saved-search.png differ diff --git a/docs/discover/images/search-session-awhile.png b/docs/discover/images/search-session-awhile.png index 0e93fe16b427d..88a6f34e10e5c 100644 Binary files a/docs/discover/images/search-session-awhile.png and b/docs/discover/images/search-session-awhile.png differ diff --git a/docs/discover/images/search-session.png b/docs/discover/images/search-session.png index c71ca6f753497..ded51feb294c1 100644 Binary files a/docs/discover/images/search-session.png and b/docs/discover/images/search-session.png differ diff --git a/docs/discover/images/search-sessions-menu.png b/docs/discover/images/search-sessions-menu.png index 508142f6e17b6..5ce8c680e90b9 100644 Binary files a/docs/discover/images/search-sessions-menu.png and b/docs/discover/images/search-sessions-menu.png differ diff --git a/docs/discover/save-search.asciidoc b/docs/discover/save-search.asciidoc index b59f14180b1ff..a9fab17bdfda2 100644 --- a/docs/discover/save-search.asciidoc +++ b/docs/discover/save-search.asciidoc @@ -6,10 +6,6 @@ that you've created in *Discover*. Saved searches are good for adding search results to a dashboard, and can also serve as a foundation for building visualizations. -[role="screenshot"] -image::discover/images/saved-search.png[Example of Discover's save search option] - - A saved search stores the query text, filters, and current view of *Discover*—the columns selected in the document table, the sort order, and the index pattern. @@ -30,6 +26,10 @@ image::discover/images/read-only-badge.png[Example of Discover's read only acces . Once you've created a search worth saving, click *Save* in the toolbar. . Enter a name for the search and click *Save*. ++ +[role="screenshot"] +image:images/discover-save-saved-search.png[Save saved search in Discover, width=50%] + . To reload your search results in *Discover*, click *Open* in the toolbar, and select the saved search. + If the saved search is associated with a different index pattern than is currently diff --git a/docs/discover/search-sessions.asciidoc b/docs/discover/search-sessions.asciidoc index 652583db785ad..d71a1bb4a99d3 100644 --- a/docs/discover/search-sessions.asciidoc +++ b/docs/discover/search-sessions.asciidoc @@ -46,7 +46,7 @@ and then click *Save session*. Once you save a search session, you can start a n navigate to a different application, or close the browser. + [role="screenshot"] -image::images/search-session-awhile.png[Search Session indicator displaying the current state of the search, which you can click to stop or save a running Search Session ] +image::images/search-session-awhile.png[Search Session indicator displaying the current state of the search, which you can click to stop or save a running Search Session, width=75% ] . To view your saved searches, open the main menu, and then click *Stack Management > Search Sessions*. You can also open this view from the search sessions popup for a saved or completed session. diff --git a/docs/discover/view-document.asciidoc b/docs/discover/view-document.asciidoc index b471e238c1a0f..26029fe58029f 100644 --- a/docs/discover/view-document.asciidoc +++ b/docs/discover/view-document.asciidoc @@ -50,7 +50,7 @@ image::images/discover-context-filters-inactive.png[Filter in context view] By default, five documents are added with each click. + [role="screenshot"] -image::images/discover-context-load-newer-documents.png[Load button and the number of documents to load] +image::images/discover-context-load-newer-documents.png[Load button and the number of documents to load, width=50%] . To configure the number of documents to display and the number of documents to load with each button click, go to *Stack Management > Advanced Settings* and edit the <>. diff --git a/docs/fleet/fleet.asciidoc b/docs/fleet/fleet.asciidoc index 4777800ce5d57..abfc14b55e020 100644 --- a/docs/fleet/fleet.asciidoc +++ b/docs/fleet/fleet.asciidoc @@ -3,8 +3,6 @@ [[fleet]] = {fleet} -beta[] - {fleet} in {kib} enables you to add and manage integrations for popular services and platforms, as well as manage {elastic-agent} installations in standalone or {fleet} mode. diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 66a23ee189ae1..32736522d583a 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -480,10 +480,11 @@ of buckets to try to represent. [horizontal] [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: -Enables the legacy charts library for aggregation-based area, line, and bar charts in *Visualize*. +**The legacy XY charts are deprecated and will not be supported as of 7.16.** +The visualize editor uses a new XY charts library with improved performance, color palettes, fill capacity, and more. Enable this option if you prefer to use the legacy charts library. [[visualization-visualize-pieChartslibrary]]`visualization:visualize:legacyPieChartsLibrary`:: -Enables the legacy charts library for aggregation-based pie charts in *Visualize*. +The visualize editor uses new pie charts with improved performance, color palettes, label positioning, and more. Enable this option if you prefer to use to the legacy charts library. [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** diff --git a/docs/management/images/management-license.png b/docs/management/images/management-license.png index 8df9402939b2e..66b9cf2bca18c 100644 Binary files a/docs/management/images/management-license.png and b/docs/management/images/management-license.png differ diff --git a/docs/management/index-patterns/images/create-index-pattern.png b/docs/management/index-patterns/images/create-index-pattern.png index 8d8efc7a6213e..67a2a2cb299d2 100644 Binary files a/docs/management/index-patterns/images/create-index-pattern.png and b/docs/management/index-patterns/images/create-index-pattern.png differ diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index d1d283ca60fbb..a523c2cb005a2 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -207,4 +207,10 @@ Use `full` to perform hostname verification, `certificate` to skip hostname veri [[alert-settings]] ==== Alerting settings -You do not need to configure any additional settings to use alerting in {kib}. +[cols="2*<"] +|=== + +| `xpack.alerting.maxEphemeralActionsPerAlert` + | Sets the number of actions that will be executed ephemerally. To use this, enable ephemeral tasks in task manager first with <> + +|=== \ No newline at end of file diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index 134d9de3f49d8..cb80165e70990 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -5,8 +5,6 @@ {fleet} settings ++++ -experimental[] - You can configure `xpack.fleet` settings in your `kibana.yml`. By default, {fleet} is enabled. To use {fleet}, you also need to configure {kib} and {es} hosts. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index a0dd8750ffc8f..455ee76deefe3 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -271,7 +271,7 @@ You can configure the following settings in the `kibana.yml` file. |[[xpack-session-idleTimeout]] `xpack.security.session.idleTimeout` {ess-icon} | Ensures that user sessions will expire after a period of inactivity. This and <> are both -highly recommended. You can also specify this setting for <>. If this is _not_ set or set to `0`, then sessions will never expire due to inactivity. By default, this setting is not set. +highly recommended. You can also specify this setting for <>. If this is set to `0`, then sessions will never expire due to inactivity. By default, this value is 1 hour. 2+a| [TIP] @@ -281,8 +281,8 @@ Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w |[[xpack-session-lifespan]] `xpack.security.session.lifespan` {ess-icon} | Ensures that user sessions will expire after the defined time period. This behavior is also known as an "absolute timeout". If -this is _not_ set or set to `0`, user sessions could stay active indefinitely. This and <> are both highly -recommended. You can also specify this setting for <>. By default, this setting is not set. +this is set to `0`, user sessions could stay active indefinitely. This and <> are both highly +recommended. You can also specify this setting for <>. By default, this value is 30 days. 2+a| [TIP] diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc index 7f4dbb3a96e6b..fa89b7780e475 100644 --- a/docs/settings/task-manager-settings.asciidoc +++ b/docs/settings/task-manager-settings.asciidoc @@ -37,6 +37,14 @@ Task Manager runs background tasks by polling for work on an interval. You can `monitored_stats_health_verbose_log.` `warn_delayed_task_start_in_seconds` | The amount of seconds we allow a task to delay before printing a warning server log. Defaults to 60. + + | `xpack.task_manager.ephemeral_tasks.enabled` + | Enables an experimental feature that executes a limited (and configurable) number of actions in the same task as the alert which triggered them. + These action tasks will reduce the latency of the time it takes an action to run after it's triggered, but are not persisted as SavedObjects. + These non-persisted action tasks have a risk that they won't be run at all if the Kibana instance running them exits unexpectedly. Defaults to false. + + | `xpack.task_manager.ephemeral_tasks.request_capacity` + | Sets the size of the ephemeral queue defined above. Defaults to 10. |=== [float] diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 5e18d934863aa..880c98902983f 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -27,7 +27,7 @@ image::images/add-data-tutorials.png[Add Data tutorials] [discrete] === Add Elastic Agent -beta[] *Elastic Agent* is a sneak peek at the next generation of +*Elastic Agent* is the next generation of data integration modules, offering a centralized way to set up your integrations. With *Fleet*, you can add diff --git a/docs/setup/images/add-data-fleet.png b/docs/setup/images/add-data-fleet.png index b6d49cfaf8d3a..279c2f5436c73 100644 Binary files a/docs/setup/images/add-data-fleet.png and b/docs/setup/images/add-data-fleet.png differ diff --git a/docs/setup/images/add-data-fv.png b/docs/setup/images/add-data-fv.png index 7e253cdd0229d..062d5ed88b3ca 100644 Binary files a/docs/setup/images/add-data-fv.png and b/docs/setup/images/add-data-fv.png differ diff --git a/docs/setup/images/add-data-home.png b/docs/setup/images/add-data-home.png index 3a844b1c40de9..b346ca190055a 100644 Binary files a/docs/setup/images/add-data-home.png and b/docs/setup/images/add-data-home.png differ diff --git a/docs/setup/images/add-data-tutorials.png b/docs/setup/images/add-data-tutorials.png index 782b44e383772..2827d7e3367f8 100644 Binary files a/docs/setup/images/add-data-tutorials.png and b/docs/setup/images/add-data-tutorials.png differ diff --git a/docs/spaces/images/edit-space-feature-visibility.png b/docs/spaces/images/edit-space-feature-visibility.png index 0132337f4a7c6..f1852d3cc03b5 100644 Binary files a/docs/spaces/images/edit-space-feature-visibility.png and b/docs/spaces/images/edit-space-feature-visibility.png differ diff --git a/docs/spaces/images/edit-space.png b/docs/spaces/images/edit-space.png index c78e0d14b0d4c..9785dd9e77aba 100644 Binary files a/docs/spaces/images/edit-space.png and b/docs/spaces/images/edit-space.png differ diff --git a/docs/user/dashboard/aggregation-based.asciidoc b/docs/user/dashboard/aggregation-based.asciidoc index cb102b73f93b4..7559cd3c1b1b8 100644 --- a/docs/user/dashboard/aggregation-based.asciidoc +++ b/docs/user/dashboard/aggregation-based.asciidoc @@ -178,4 +178,3 @@ image:images/bar-chart-tutorial-2.png[Bar chart with sample logs data] - diff --git a/docs/user/security/session-management.asciidoc b/docs/user/security/session-management.asciidoc index ac7a777eb0580..b0f27d45bb826 100644 --- a/docs/user/security/session-management.asciidoc +++ b/docs/user/security/session-management.asciidoc @@ -12,24 +12,24 @@ To manage user sessions programmatically, {kib} exposes <[ms|s|m|h|d|w|M|Y]` (e.g. '20m', '24h', '7d', '1w'). For example, set the idle timeout to expire sessions after 1 hour of inactivity: +By default, sessions expire after 1 hour of inactivity. To define another value for a sliding session expiration, set the property in the `kibana.yml` configuration file. The idle timeout is formatted as a duration of `[ms|s|m|h|d|w|M|Y]` (e.g. '20m', '24h', '7d', '1w'). For example, set the idle timeout to expire sessions after 30 minutes of inactivity: -- [source,yaml] -------------------------------------------------------------------------------- -xpack.security.session.idleTimeout: "1h" +xpack.security.session.idleTimeout: "30m" -------------------------------------------------------------------------------- -- [[session-lifespan]] ==== Session lifespan -You can use `xpack.security.session.lifespan` to configure the maximum session duration or "lifespan" -- also known as the "absolute timeout". This and `xpack.security.session.idleTimeout` are both highly recommended. By default, sessions don't have a fixed lifespan, and if an idle timeout is defined, a session can still be extended indefinitely. To define a maximum session lifespan, set the property in the `kibana.yml` configuration file. The lifespan is formatted as a duration of `[ms|s|m|h|d|w|M|Y]` (e.g. '20m', '24h', '7d', '1w'). For example, set the lifespan to expire sessions after 30 days: +You can use `xpack.security.session.lifespan` to configure the maximum session duration or "lifespan" -- also known as the "absolute timeout". This and `xpack.security.session.idleTimeout` are both highly recommended. By default, a maximum session lifespan is 30 days. To define another lifespan, set the property in the `kibana.yml` configuration file. The lifespan is formatted as a duration of `[ms|s|m|h|d|w|M|Y]` (e.g. '20m', '24h', '7d', '1w'). For example, set the lifespan to expire sessions after 7 days: -- [source,yaml] -------------------------------------------------------------------------------- -xpack.security.session.lifespan: "30d" +xpack.security.session.lifespan: "7d" -------------------------------------------------------------------------------- -- @@ -38,7 +38,7 @@ xpack.security.session.lifespan: "30d" [IMPORTANT] ============================================================================ -If you specify neither session idle timeout nor lifespan, then {kib} will not automatically remove session information from the index unless you explicitly log out. This might lead to an infinitely growing session index. Configure the idle timeout and lifespan settings for the {kib} sessions so that they can be cleaned up even if you don't explicitly log out. +If you disable session idle timeout and lifespan, then Kibana will not automatically remove session information from the index unless you explicitly log out. This might lead to an infinitely growing session index. As long as either idle timeout or lifespan is configured, Kibana sessions will be cleaned up even if you don't explicitly log out. ============================================================================ You can configure the interval at which {kib} tries to remove expired and invalid sessions from the session index. By default, this value is 1 hour and cannot be less than 10 seconds. To define another interval, set the `xpack.security.session.cleanupInterval` property in the `kibana.yml` configuration file. The interval is formatted as a duration of `[ms|s|m|h|d|w|M|Y]` (e.g. '20m', '24h', '7d', '1w'). For example, schedule the session index cleanup to perform once a day: diff --git a/examples/preboot_example/README.md b/examples/preboot_example/README.md new file mode 100644 index 0000000000000..0f3ab6c6eae2a --- /dev/null +++ b/examples/preboot_example/README.md @@ -0,0 +1,3 @@ +# `prebootExample` plugin + +The example of the `preboot` plugin. \ No newline at end of file diff --git a/examples/preboot_example/kibana.json b/examples/preboot_example/kibana.json new file mode 100644 index 0000000000000..b8b5ceb1a9c6c --- /dev/null +++ b/examples/preboot_example/kibana.json @@ -0,0 +1,16 @@ +{ + "id": "prebootExample", + "owner": { + "name": "Core", + "githubTeam": "kibana-core" + }, + "description": "The example of the `preboot` plugin.", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["prebootExample"], + "type": "preboot", + "server": true, + "ui": true, + "requiredPlugins": [], + "requiredBundles": [] +} diff --git a/examples/preboot_example/public/app.tsx b/examples/preboot_example/public/app.tsx new file mode 100644 index 0000000000000..364b7d5bfe8d3 --- /dev/null +++ b/examples/preboot_example/public/app.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiButton, + EuiCodeBlock, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiPageTemplate, + EuiPanel, + EuiText, +} from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import type { HttpSetup, IHttpFetchError } from 'src/core/public'; + +export const App = ({ http, token }: { http: HttpSetup; token?: string }) => { + const onCompleteSetup = async ({ shouldReloadConfig }: { shouldReloadConfig: boolean }) => { + await http + .post('/api/preboot/complete_setup', { + body: JSON.stringify({ shouldReloadConfig }), + }) + .then(() => { + setTimeout(() => { + window.location.href = '/'; + }, 5000); + }); + }; + + const onWriteToken = async () => { + await http.post('/api/preboot/write_config', { body: JSON.stringify(configKeyValue) }); + }; + + const onConnect = async () => { + await http + .post('/api/preboot/connect_to_es', { body: JSON.stringify(elasticsearchConfig) }) + .then( + (response) => setConnectResponse(JSON.stringify(response)), + (err: IHttpFetchError) => setConnectResponse(err?.body?.message || 'ERROR') + ); + }; + + const [configKeyValue, setConfigKeyValue] = useState<{ key: string; value: string }>({ + key: '', + value: '', + }); + + const [elasticsearchConfig, setElasticsearchConfig] = useState<{ + host: string; + username: string; + password: string; + }>({ + host: 'http://localhost:9200', + username: 'kibana_system', + password: '', + }); + + const [connectResponse, setConnectResponse] = useState(null); + + const [isSetupModeActive, setIsSetupModeActive] = useState(false); + useEffect(() => { + http.get<{ isSetupModeActive: boolean }>('/api/preboot/state').then( + (response) => setIsSetupModeActive(response.isSetupModeActive), + (err: IHttpFetchError) => setIsSetupModeActive(false) + ); + }, [http]); + + if (!isSetupModeActive) { + return ( + + + Kibana server is not ready yet. + + + ); + } + + return ( + + + + + + + { + setConfigKeyValue({ ...configKeyValue, key: e.target.value }); + }} + /> + + + { + setConfigKeyValue({ ...configKeyValue, value: e.target.value }); + }} + /> + + + + Write config + + + + + + + + Token from config: {token} + + + onCompleteSetup({ shouldReloadConfig: true })} + > + Reload config and proceed to `setup` + + + + onCompleteSetup({ shouldReloadConfig: false })} + > + DO NOT reload config and proceed to `setup` + + + + + + + + { + setElasticsearchConfig({ ...elasticsearchConfig, host: e.target.value }); + }} + /> + + + { + setElasticsearchConfig({ + ...elasticsearchConfig, + username: e.target.value, + }); + }} + /> + + + { + setElasticsearchConfig({ + ...elasticsearchConfig, + password: e.target.value, + }); + }} + /> + + + + Connect + + + + + + + {connectResponse ?? ''} + + + + + + ); +}; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts b/examples/preboot_example/public/config.ts similarity index 87% rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts rename to examples/preboot_example/public/config.ts index b03ee16d2f746..fc91296ce3724 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts +++ b/examples/preboot_example/public/config.ts @@ -6,4 +6,6 @@ * Side Public License, v 1. */ -// stub +export interface ConfigType { + token?: string; +} diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts b/examples/preboot_example/public/index.ts similarity index 61% rename from src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts rename to examples/preboot_example/public/index.ts index 09cd26c9e620b..7859758c274f3 100644 --- a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts +++ b/examples/preboot_example/public/index.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { errors as revealImage } from './dict/reveal_image'; +import { PluginInitializerContext } from 'kibana/public'; +import { PrebootExamplePlugin } from './plugin'; -export const getFunctionErrors = () => ({ - revealImage, -}); +export const plugin = (initializerContext: PluginInitializerContext) => + new PrebootExamplePlugin(initializerContext); diff --git a/examples/preboot_example/public/plugin.tsx b/examples/preboot_example/public/plugin.tsx new file mode 100644 index 0000000000000..1db38e2240a86 --- /dev/null +++ b/examples/preboot_example/public/plugin.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { App } from './app'; +import { ConfigType } from './config'; + +export class PrebootExamplePlugin implements Plugin { + #config: ConfigType; + constructor(initializerContext: PluginInitializerContext) { + this.#config = initializerContext.config.get(); + } + + public setup(core: CoreSetup) { + core.application.register({ + id: 'prebootExample', + title: 'Preboot Example', + appRoute: '/', + chromeless: true, + mount: (params) => { + ReactDOM.render(, params.element); + return () => ReactDOM.unmountComponentAtNode(params.element); + }, + }); + } + + public start(core: CoreStart) {} +} diff --git a/src/plugins/expression_reveal_image/common/i18n/constants.ts b/examples/preboot_example/server/config.ts similarity index 50% rename from src/plugins/expression_reveal_image/common/i18n/constants.ts rename to examples/preboot_example/server/config.ts index 413f376515a33..db39f985bca30 100644 --- a/src/plugins/expression_reveal_image/common/i18n/constants.ts +++ b/examples/preboot_example/server/config.ts @@ -6,5 +6,13 @@ * Side Public License, v 1. */ -export const BASE64 = '`base64`'; -export const URL = 'URL'; +import type { TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; + +export type ConfigType = TypeOf; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + token: schema.maybe(schema.string()), + skipSetup: schema.boolean({ defaultValue: false }), +}); diff --git a/examples/preboot_example/server/index.ts b/examples/preboot_example/server/index.ts new file mode 100644 index 0000000000000..0377250d0cf50 --- /dev/null +++ b/examples/preboot_example/server/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import type { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; + +import { ConfigSchema } from './config'; +import { PrebootExamplePlugin } from './plugin'; + +export const config: PluginConfigDescriptor> = { + schema: ConfigSchema, + exposeToBrowser: { token: true }, +}; + +export const plugin = (context: PluginInitializerContext) => new PrebootExamplePlugin(context); diff --git a/examples/preboot_example/server/plugin.ts b/examples/preboot_example/server/plugin.ts new file mode 100644 index 0000000000000..a3c1e9d199143 --- /dev/null +++ b/examples/preboot_example/server/plugin.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import type { CorePreboot, PrebootPlugin, PluginInitializerContext } from 'src/core/server'; +import fs from 'fs/promises'; +import { errors } from '@elastic/elasticsearch'; +import Boom from '@hapi/boom'; +import type { ConfigType } from './config'; + +export function getDetailedErrorMessage(error: any): string { + if (error instanceof errors.ResponseError) { + return JSON.stringify(error.body); + } + + if (Boom.isBoom(error)) { + return JSON.stringify(error.output.payload); + } + + return error.message; +} + +export class PrebootExamplePlugin implements PrebootPlugin { + readonly #initializerContext: PluginInitializerContext; + constructor(initializerContext: PluginInitializerContext) { + this.#initializerContext = initializerContext; + } + + public setup(core: CorePreboot) { + const { skipSetup } = this.#initializerContext.config.get(); + let completeSetup: (result: { shouldReloadConfig: boolean }) => void; + + core.http.registerRoutes('', (prebootRouter) => { + prebootRouter.get( + { + path: '/api/preboot/state', + validate: false, + options: { authRequired: false }, + }, + (_, request, response) => { + const isSetupModeActive = !skipSetup && core.preboot.isSetupOnHold(); + return response.ok({ body: { isSetupModeActive } }); + } + ); + if (skipSetup) { + return; + } + + prebootRouter.post( + { + path: '/api/preboot/complete_setup', + validate: { + body: schema.object({ shouldReloadConfig: schema.boolean() }), + }, + options: { authRequired: false }, + }, + (_, request, response) => { + completeSetup({ shouldReloadConfig: request.body.shouldReloadConfig }); + return response.noContent(); + } + ); + + prebootRouter.post( + { + path: '/api/preboot/write_config', + validate: { + body: schema.object({ key: schema.string(), value: schema.string() }), + }, + options: { authRequired: false }, + }, + async (_, request, response) => { + const configPath = this.#initializerContext.env.configs.find((path) => + path.includes('dev') + ); + + if (!configPath) { + return response.customError({ statusCode: 500, body: 'Cannot find dev config.' }); + } + + await fs.appendFile(configPath, `${request.body.key}: ${request.body.value}\n`); + return response.noContent(); + } + ); + + prebootRouter.post( + { + path: '/api/preboot/connect_to_es', + validate: { + body: schema.object({ + host: schema.string(), + username: schema.string(), + password: schema.string(), + }), + }, + options: { authRequired: false }, + }, + async (_, request, response) => { + const esClient = core.elasticsearch.createClient('data', { + hosts: [request.body.host], + }); + + const scopedClient = esClient.asScoped({ + headers: { + authorization: `Basic ${Buffer.from( + `${request.body.username}:${request.body.password}` + ).toString('base64')}`, + }, + }); + + try { + return response.ok({ + body: (await scopedClient.asCurrentUser.security.authenticate()).body, + }); + } catch (err) { + return response.customError({ statusCode: 500, body: getDetailedErrorMessage(err) }); + } + } + ); + + core.preboot.holdSetupUntilResolved( + 'Elasticsearch connection is not set up', + new Promise<{ shouldReloadConfig: boolean }>((resolve) => { + completeSetup = resolve; + }) + ); + }); + } + + public stop() {} +} diff --git a/examples/preboot_example/tsconfig.json b/examples/preboot_example/tsconfig.json new file mode 100644 index 0000000000000..d18953eadf330 --- /dev/null +++ b/examples/preboot_example/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "server/**/*"], + "references": [{ "path": "../../src/core/tsconfig.json" }] +} diff --git a/package.json b/package.json index 0875242a18da9..85343fd8023d0 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "dependencies": { "@elastic/apm-rum": "^5.8.0", "@elastic/apm-rum-react": "^1.2.11", - "@elastic/charts": "32.0.0", + "@elastic/charts": "33.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.14.0", diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index b1b622381abb1..d09c61a1c2110 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -37,6 +37,7 @@ const getRawConfigProvider = (rawConfig: Record) => beforeEach(() => { logger = loggerMock.create(); + mockApplyDeprecations.mockClear(); }); test('returns config at path as observable', async () => { @@ -485,6 +486,16 @@ test('does not log warnings for silent deprecations during validation', async () expect(loggerMock.collect(logger).warn).toMatchInlineSnapshot(`Array []`); }); +test('does not log warnings during validation if specifically requested', async () => { + const configService = new ConfigService(getRawConfigProvider({}), defaultEnv, logger); + loggerMock.clear(logger); + + await configService.validate({ logDeprecations: false }); + + expect(mockApplyDeprecations).not.toHaveBeenCalled(); + expect(loggerMock.collect(logger).warn).toMatchInlineSnapshot(`Array []`); +}); + describe('atPathSync', () => { test('returns the value at path', async () => { const rawConfig = getRawConfigProvider({ key: 'foo' }); diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index a80680bd46dfc..514992891ad1b 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -29,6 +29,14 @@ import { LegacyObjectToConfigAdapter } from './legacy'; /** @internal */ export type IConfigService = PublicMethodsOf; +/** @internal */ +export interface ConfigValidateParameters { + /** + * Indicates whether config deprecations should be logged during validation. + */ + logDeprecations: boolean; +} + /** @internal */ export class ConfigService { private readonly log: Logger; @@ -111,13 +119,16 @@ export class ConfigService { * * This must be done after every schemas and deprecation providers have been registered. */ - public async validate() { + public async validate(params: ConfigValidateParameters = { logDeprecations: true }) { const namespaces = [...this.schemas.keys()]; for (let i = 0; i < namespaces.length; i++) { await this.getValidatedConfigAtPath$(namespaces[i]).pipe(first()).toPromise(); } - await this.logDeprecation(); + if (params.logDeprecations) { + await this.logDeprecation(); + } + this.validated = true; } diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index 294caba4e7048..08cf12343f459 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -25,7 +25,7 @@ export { getConfigFromFiles, } from './raw'; -export { ConfigService, IConfigService } from './config_service'; +export { ConfigService, IConfigService, ConfigValidateParameters } from './config_service'; export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config'; export { ObjectToConfigAdapter } from './object_to_config_adapter'; export { CliArgs, Env, RawPackageInfo } from './env'; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 2f6765cd57b9e..49d4bdbc2de64 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -107,7 +107,7 @@ pageLoadAssetSize: dataVisualizer: 27530 banners: 17946 mapsEms: 26072 - timelines: 251886 + timelines: 330000 screenshotMode: 17856 visTypePie: 35583 expressionRevealImage: 25675 diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index 48d36b706b831..97a7f33be673d 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -15,7 +15,7 @@ import cpy from 'cpy'; import del from 'del'; import { tap, filter } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/utils'; -import { ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog, createReplaceSerializer } from '@kbn/dev-utils'; import { runOptimizer, OptimizerConfig, OptimizerUpdate, logOptimizerState } from '../index'; import { allValuesFrom } from '../common'; @@ -29,6 +29,8 @@ expect.addSnapshotSerializer({ test: (value: any) => typeof value === 'string' && value.includes(REPO_ROOT), }); +expect.addSnapshotSerializer(createReplaceSerializer(/\w+-fastbuild/, '-fastbuild')); + const log = new ToolingLog({ level: 'error', writeTo: { @@ -130,7 +132,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(foo.cache.getModuleCount()).toBe(6); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, @@ -153,7 +155,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /node_modules/@kbn/optimizer/postcss.config.js, /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, @@ -173,7 +175,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(baz.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/public/index.ts, /packages/kbn-optimizer/src/worker/entry_point_creator.ts, diff --git a/packages/kbn-optimizer/src/optimizer/watcher.ts b/packages/kbn-optimizer/src/optimizer/watcher.ts index d0420d3c3699d..65958d6669f73 100644 --- a/packages/kbn-optimizer/src/optimizer/watcher.ts +++ b/packages/kbn-optimizer/src/optimizer/watcher.ts @@ -38,7 +38,6 @@ export class Watcher { private readonly watchpack = new Watchpack({ aggregateTimeout: 0, - ignored: /node_modules\/([^\/]+[\/])*(?!package.json)([^\/]+)$/, }); private readonly change$ = Rx.fromEvent<[string]>(this.watchpack, 'change').pipe(share()); diff --git a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts index a3455d7ddf2b9..bc8418811e7ae 100644 --- a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts +++ b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import Fs from 'fs'; import Path from 'path'; import { inspect } from 'util'; @@ -21,20 +22,6 @@ import { getModulePath, } from './webpack_helpers'; -function tryToResolveRewrittenPath(from: string, toResolve: string) { - try { - return require.resolve(toResolve); - } catch (error) { - if (error.code === 'MODULE_NOT_FOUND') { - throw new Error( - `attempted to rewrite bazel-out path [${from}] to [${toResolve}] but couldn't find the rewrite target` - ); - } - - throw error; - } -} - /** * sass-loader creates about a 40% overhead on the overall optimizer runtime, and * so this constant is used to indicate to assignBundlesToWorkers() that there is @@ -44,6 +31,20 @@ function tryToResolveRewrittenPath(from: string, toResolve: string) { */ const EXTRA_SCSS_WORK_UNITS = 100; +const isBazelPackageCache = new Map(); +function isBazelPackage(pkgJsonPath: string) { + const cached = isBazelPackageCache.get(pkgJsonPath); + if (typeof cached === 'boolean') { + return cached; + } + + const path = parseFilePath(Fs.realpathSync(pkgJsonPath, 'utf-8')); + const match = !!path.matchDirs('bazel-out', /-fastbuild$/, 'bin', 'packages'); + isBazelPackageCache.set(pkgJsonPath, match); + + return match; +} + export class PopulateBundleCachePlugin { constructor(private readonly workerConfig: WorkerConfig, private readonly bundle: Bundle) {} @@ -71,44 +72,16 @@ export class PopulateBundleCachePlugin { let path = getModulePath(module); let parsedPath = parseFilePath(path); - const bazelOut = parsedPath.matchDirs( - 'bazel-out', - /-fastbuild$/, - 'bin', - 'packages', - /.*/, - 'target' - ); - - // if the module is referenced from one of our packages and resolved to the `bazel-out` dir - // we should rewrite our reference to point to the source file so that we can track the - // modified time of that file rather than the built output which is rebuilt all the time - // without actually changing - if (bazelOut) { - const packageDir = parsedPath.dirs[bazelOut.endIndex - 1]; - const subDirs = parsedPath.dirs.slice(bazelOut.endIndex + 1); - path = tryToResolveRewrittenPath( - path, - Path.join( - workerConfig.repoRoot, - 'packages', - packageDir, - 'src', - ...subDirs, - parsedPath.filename - ? Path.basename(parsedPath.filename, Path.extname(parsedPath.filename)) - : '' - ) + const bazelOutIndex = parsedPath.dirs.indexOf('bazel-out'); + if (bazelOutIndex >= 0) { + path = Path.resolve( + this.workerConfig.repoRoot, + ...parsedPath.dirs.slice(bazelOutIndex), + parsedPath.filename ?? '' ); parsedPath = parseFilePath(path); } - if (parsedPath.matchDirs('bazel-out')) { - throw new Error( - `a bazel-out dir is being referenced by module [${path}] and not getting rewritten to its source location` - ); - } - if (!parsedPath.dirs.includes('node_modules')) { referencedFiles.add(path); @@ -125,13 +98,13 @@ export class PopulateBundleCachePlugin { const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); - referencedFiles.add( - Path.join( - parsedPath.root, - ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), - 'package.json' - ) + const pkgJsonPath = Path.join( + parsedPath.root, + ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + 'package.json' ); + + referencedFiles.add(isBazelPackage(pkgJsonPath) ? path : pkgJsonPath); continue; } diff --git a/src/core/TESTING.md b/src/core/TESTING.md index 10ead1ea8cfe2..5e0913e7a9d20 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -337,6 +337,7 @@ describe('myPlugin', () => { let root: ReturnType; beforeAll(async () => { root = kbnTestServer.createRoot(); + await root.preboot(); await root.setup(); await root.start(); }, 30000); @@ -382,6 +383,7 @@ describe('myPlugin', () => { let root: ReturnType; beforeAll(async () => { root = kbnTestServer.createRoot(); + await root.preboot(); await root.setup(); await root.start(); }, 30000); diff --git a/src/core/public/execution_context/execution_context_container.ts b/src/core/public/execution_context/execution_context_container.ts index 9c8e3e269ec88..8e0670472c80d 100644 --- a/src/core/public/execution_context/execution_context_container.ts +++ b/src/core/public/execution_context/execution_context_container.ts @@ -30,6 +30,7 @@ function enforceMaxLength(header: string): string { */ export interface IExecutionContextContainer { toHeader: () => Record; + toJSON: () => Readonly; } export class ExecutionContextContainer implements IExecutionContextContainer { @@ -42,7 +43,12 @@ export class ExecutionContextContainer implements IExecutionContextContainer { // escape content as the description property might contain non-ASCII symbols return enforceMaxLength(encodeURIComponent(value)); } + toHeader() { return { [BAGGAGE_HEADER]: this.toString() }; } + + toJSON() { + return this.#context; + } } diff --git a/src/core/public/execution_context/execution_context_service.mock.ts b/src/core/public/execution_context/execution_context_service.mock.ts index d8148b0af807d..071e61f17c25c 100644 --- a/src/core/public/execution_context/execution_context_service.mock.ts +++ b/src/core/public/execution_context/execution_context_service.mock.ts @@ -13,6 +13,7 @@ import type { ExecutionContextContainer } from './execution_context_container'; const createContainerMock = () => { const mock: jest.Mocked> = { toHeader: jest.fn(), + toJSON: jest.fn(), }; return mock; }; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index b3dd3827352bd..40304d27580ca 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -186,16 +186,14 @@ export type { export type { DeprecationsServiceStart, ResolveDeprecationResponse } from './deprecations'; -export type { - IExecutionContextContainer, - ExecutionContextServiceStart, - KibanaExecutionContext, -} from './execution_context'; +export type { IExecutionContextContainer, ExecutionContextServiceStart } from './execution_context'; export type { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types'; export { URL_MAX_LENGTH } from './core_app'; +export type { KibanaExecutionContext } from './execution_context'; + /** * Core services exposed to the `Plugin` setup lifecycle * diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index ef919018f120b..94c88f732f4e1 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -8,7 +8,7 @@ import { mockInitializer, mockPlugin, mockPluginReader } from './plugin.test.mocks'; -import { DiscoveredPlugin } from '../../server'; +import { DiscoveredPlugin, PluginType } from '../../server'; import { coreMock } from '../mocks'; import { PluginWrapper } from './plugin'; @@ -19,6 +19,7 @@ function createManifest( return { id, version: 'some-version', + type: PluginType.standard, configPath: ['path'], requiredPlugins: required, optionalPlugins: optional, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index d62a4bcdd1e51..3f23889c57de6 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -13,7 +13,7 @@ import { mockPluginInitializerProvider, } from './plugins_service.test.mocks'; -import { PluginName } from 'src/core/server'; +import { PluginName, PluginType } from 'src/core/server'; import { coreMock } from '../mocks'; import { PluginsService, @@ -60,6 +60,7 @@ function createManifest( return { id, version: 'some-version', + type: PluginType.standard, configPath: ['path'], requiredPlugins: required, optionalPlugins: optional, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d23980ff55a2c..673cb2e7ab557 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -900,6 +900,8 @@ export interface IBasePath { export interface IExecutionContextContainer { // (undocumented) toHeader: () => Record; + // (undocumented) + toJSON: () => Readonly; } // @public diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index 18a5eceb1b2d3..5131defc93461 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -59,9 +59,9 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot reloadConfiguration(); }); - function reloadConfiguration() { + function reloadConfiguration(reason = 'SIGHUP signal received') { const cliLogger = root.logger.get('cli'); - cliLogger.info('Reloading Kibana configuration due to SIGHUP.', { tags: ['config'] }); + cliLogger.info(`Reloading Kibana configuration (reason: ${reason}).`, { tags: ['config'] }); try { rawConfigService.reloadConfig(); @@ -69,7 +69,7 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot return shutdown(err); } - cliLogger.info('Reloaded Kibana configuration due to SIGHUP.', { tags: ['config'] }); + cliLogger.info(`Reloaded Kibana configuration (reason: ${reason}).`, { tags: ['config'] }); } process.on('SIGINT', () => shutdown()); @@ -81,11 +81,28 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot } try { + const { preboot } = await root.preboot(); + + // If setup is on hold then preboot server is supposed to serve user requests and we can let + // dev parent process know that we are ready for dev mode. + const isSetupOnHold = preboot.isSetupOnHold(); + if (process.send && isSetupOnHold) { + process.send(['SERVER_LISTENING']); + } + + if (isSetupOnHold) { + root.logger.get().info('Holding setup until preboot stage is completed.'); + const { shouldReloadConfig } = await preboot.waitUntilCanSetup(); + if (shouldReloadConfig) { + await reloadConfiguration('configuration might have changed during preboot stage'); + } + } + await root.setup(); await root.start(); - // notify parent process know when we are ready for dev mode. - if (process.send) { + // Notify parent process if we haven't done that yet during preboot stage. + if (process.send && !isSetupOnHold) { process.send(['SERVER_LISTENING']); } } catch (err) { diff --git a/src/core/server/capabilities/capabilities_service.mock.ts b/src/core/server/capabilities/capabilities_service.mock.ts index 0ba3446a77270..1af10b3ad981e 100644 --- a/src/core/server/capabilities/capabilities_service.mock.ts +++ b/src/core/server/capabilities/capabilities_service.mock.ts @@ -36,6 +36,7 @@ const createCapabilitiesMock = (): Capabilities => { type CapabilitiesServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { + preboot: jest.fn(), setup: jest.fn().mockReturnValue(createSetupContractMock()), start: jest.fn().mockReturnValue(createStartContractMock()), }; diff --git a/src/core/server/capabilities/capabilities_service.test.ts b/src/core/server/capabilities/capabilities_service.test.ts index 8f9627a64b082..0476f844d011f 100644 --- a/src/core/server/capabilities/capabilities_service.test.ts +++ b/src/core/server/capabilities/capabilities_service.test.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { httpServiceMock, InternalHttpServiceSetupMock } from '../http/http_service.mock'; +import { + httpServiceMock, + InternalHttpServicePrebootMock, + InternalHttpServiceSetupMock, +} from '../http/http_service.mock'; import { mockRouter, RouterMock } from '../http/router/router.mock'; import { CapabilitiesService, CapabilitiesSetup } from './capabilities_service'; import { mockCoreContext } from '../core_context.mock'; @@ -24,6 +28,31 @@ describe('CapabilitiesService', () => { service = new CapabilitiesService(mockCoreContext.create()); }); + describe('#preboot()', () => { + let httpPreboot: InternalHttpServicePrebootMock; + beforeEach(() => { + httpPreboot = httpServiceMock.createInternalPrebootContract(); + service.preboot({ http: httpPreboot }); + }); + + it('registers the capabilities routes', async () => { + expect(httpPreboot.registerRoutes).toHaveBeenCalledWith('', expect.any(Function)); + expect(httpPreboot.registerRoutes).toHaveBeenCalledTimes(1); + + const [[, callback]] = httpPreboot.registerRoutes.mock.calls; + callback(router); + + expect(router.post).toHaveBeenCalledTimes(1); + expect(router.post).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/core/capabilities', + options: { authRequired: 'optional' }, + }), + expect.any(Function) + ); + }); + }); + describe('#setup()', () => { beforeEach(() => { setup = service.setup({ http }); diff --git a/src/core/server/capabilities/capabilities_service.ts b/src/core/server/capabilities/capabilities_service.ts index 6088ec29db998..1166c8f6b48c4 100644 --- a/src/core/server/capabilities/capabilities_service.ts +++ b/src/core/server/capabilities/capabilities_service.ts @@ -9,7 +9,7 @@ import { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; -import { InternalHttpServiceSetup, KibanaRequest } from '../http'; +import { InternalHttpServicePreboot, InternalHttpServiceSetup, KibanaRequest } from '../http'; import { mergeCapabilities } from './merge_capabilities'; import { getCapabilitiesResolver, CapabilitiesResolver } from './resolve_capabilities'; import { registerRoutes } from './routes'; @@ -120,6 +120,10 @@ export interface CapabilitiesStart { ): Promise; } +interface PrebootSetupDeps { + http: InternalHttpServicePreboot; +} + interface SetupDeps { http: InternalHttpServiceSetup; } @@ -149,10 +153,20 @@ export class CapabilitiesService { ); } + public preboot(prebootDeps: PrebootSetupDeps) { + this.logger.debug('Prebooting capabilities service'); + + // The preboot server has no need for real capabilities. + // Returning the un-augmented defaults is sufficient. + prebootDeps.http.registerRoutes('', (router) => { + registerRoutes(router, async () => defaultCapabilities); + }); + } + public setup(setupDeps: SetupDeps): CapabilitiesSetup { this.logger.debug('Setting up capabilities service'); - registerRoutes(setupDeps.http, this.resolveCapabilities); + registerRoutes(setupDeps.http.createRouter(''), this.resolveCapabilities); return { registerProvider: (provider: CapabilitiesProvider) => { diff --git a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts index ac793d960d03b..2e80fbb9d20c0 100644 --- a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts +++ b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts @@ -8,7 +8,7 @@ import supertest from 'supertest'; import { REPO_ROOT } from '@kbn/dev-utils'; -import { HttpService, InternalHttpServiceSetup } from '../../http'; +import { HttpService, InternalHttpServicePreboot, InternalHttpServiceSetup } from '../../http'; import { contextServiceMock } from '../../context/context_service.mock'; import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; @@ -23,6 +23,7 @@ const env = Env.createDefault(REPO_ROOT, getEnvOptions()); describe('CapabilitiesService', () => { let server: HttpService; + let httpPreboot: InternalHttpServicePreboot; let httpSetup: InternalHttpServiceSetup; let service: CapabilitiesService; @@ -30,6 +31,7 @@ describe('CapabilitiesService', () => { beforeEach(async () => { server = createHttpServer(); + httpPreboot = await server.preboot({ context: contextServiceMock.createPrebootContract() }); httpSetup = await server.setup({ context: contextServiceMock.createSetupContract(), executionContext: executionContextServiceMock.createInternalSetupContract(), @@ -40,6 +42,7 @@ describe('CapabilitiesService', () => { logger: loggingSystemMock.create(), configService: {} as any, }); + await service.preboot({ http: httpPreboot }); serviceSetup = await service.setup({ http: httpSetup }); await server.start(); }); diff --git a/src/core/server/capabilities/routes/index.ts b/src/core/server/capabilities/routes/index.ts index a417dbfeb43a3..4140d75d1a1a1 100644 --- a/src/core/server/capabilities/routes/index.ts +++ b/src/core/server/capabilities/routes/index.ts @@ -7,10 +7,9 @@ */ import { CapabilitiesResolver } from '../resolve_capabilities'; -import { InternalHttpServiceSetup } from '../../http'; +import { IRouter } from '../../http'; import { registerCapabilitiesRoutes } from './resolve_capabilities'; -export function registerRoutes(http: InternalHttpServiceSetup, resolver: CapabilitiesResolver) { - const router = http.createRouter(''); +export function registerRoutes(router: IRouter, resolver: CapabilitiesResolver) { registerCapabilitiesRoutes(router, resolver); } diff --git a/src/core/server/config/ensure_valid_configuration.test.ts b/src/core/server/config/ensure_valid_configuration.test.ts index f1006f93dbc2d..372b9d4c0dfad 100644 --- a/src/core/server/config/ensure_valid_configuration.test.ts +++ b/src/core/server/config/ensure_valid_configuration.test.ts @@ -23,6 +23,20 @@ describe('ensureValidConfiguration', () => { it('returns normally when there is no unused keys and when the config validates', async () => { await expect(ensureValidConfiguration(configService as any)).resolves.toBeUndefined(); + + expect(configService.validate).toHaveBeenCalledWith(undefined); + }); + + it('forwards parameters to the `validate` method', async () => { + await expect( + ensureValidConfiguration(configService as any, { logDeprecations: false }) + ).resolves.toBeUndefined(); + expect(configService.validate).toHaveBeenCalledWith({ logDeprecations: false }); + + await expect( + ensureValidConfiguration(configService as any, { logDeprecations: true }) + ).resolves.toBeUndefined(); + expect(configService.validate).toHaveBeenCalledWith({ logDeprecations: true }); }); it('throws when config validation fails', async () => { diff --git a/src/core/server/config/ensure_valid_configuration.ts b/src/core/server/config/ensure_valid_configuration.ts index c7a4721b7d2ae..040cb23e07988 100644 --- a/src/core/server/config/ensure_valid_configuration.ts +++ b/src/core/server/config/ensure_valid_configuration.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ConfigService } from '@kbn/config'; +import { ConfigService, ConfigValidateParameters } from '@kbn/config'; import { CriticalError } from '../errors'; const ignoredPaths = ['dev.', 'elastic.apm.']; @@ -14,9 +14,12 @@ const ignoredPaths = ['dev.', 'elastic.apm.']; const invalidConfigExitCode = 78; const legacyInvalidConfigExitCode = 64; -export async function ensureValidConfiguration(configService: ConfigService) { +export async function ensureValidConfiguration( + configService: ConfigService, + params?: ConfigValidateParameters +) { try { - await configService.validate(); + await configService.validate(params); } catch (e) { throw new CriticalError(e.message, 'InvalidConfig', invalidConfigExitCode, e); } diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts index 5b672774c515a..2d86281ce40d6 100644 --- a/src/core/server/config/integration_tests/config_deprecation.test.ts +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -26,6 +26,7 @@ describe('configuration deprecations', () => { it('should not log deprecation warnings for default configuration that is not one of `logging.verbose`, `logging.quiet` or `logging.silent`', async () => { root = kbnTestServer.createRoot(); + await root.preboot(); await root.setup(); const logs = loggingSystemMock.collect(mockLoggingSystem); @@ -44,6 +45,7 @@ describe('configuration deprecations', () => { }, }); + await root.preboot(); await root.setup(); const logs = loggingSystemMock.collect(mockLoggingSystem); diff --git a/src/core/server/context/context_service.mock.ts b/src/core/server/context/context_service.mock.ts index e705fff2e35a5..c9b1e0e7692e5 100644 --- a/src/core/server/context/context_service.mock.ts +++ b/src/core/server/context/context_service.mock.ts @@ -8,9 +8,16 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { ContextService, ContextSetup } from './context_service'; +import { ContextService, ContextSetup, InternalContextPreboot } from './context_service'; import { contextMock } from './container/context.mock'; +const createPrebootContractMock = (mockContext = {}) => { + const prebootContract: jest.Mocked = { + createContextContainer: jest.fn().mockImplementation(() => contextMock.create(mockContext)), + }; + return prebootContract; +}; + const createSetupContractMock = (mockContext = {}) => { const setupContract: jest.Mocked = { createContextContainer: jest.fn().mockImplementation(() => contextMock.create(mockContext)), @@ -21,13 +28,16 @@ const createSetupContractMock = (mockContext = {}) => { type ContextServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { + preboot: jest.fn(), setup: jest.fn(), }; + mocked.preboot.mockReturnValue(createPrebootContractMock()); mocked.setup.mockReturnValue(createSetupContractMock()); return mocked; }; export const contextServiceMock = { create: createMock, + createPrebootContract: createPrebootContractMock, createSetupContract: createSetupContractMock, }; diff --git a/src/core/server/context/context_service.test.ts b/src/core/server/context/context_service.test.ts index 48dc485727251..8b5a784b1f8a1 100644 --- a/src/core/server/context/context_service.test.ts +++ b/src/core/server/context/context_service.test.ts @@ -14,10 +14,23 @@ import { CoreContext } from '../core_context'; const pluginDependencies = new Map(); describe('ContextService', () => { + describe('#preboot()', () => { + test('createContextContainer returns a new container configured with pluginDependencies', () => { + const coreId = Symbol(); + const service = new ContextService({ coreId } as CoreContext); + const preboot = service.preboot({ pluginDependencies }); + expect(preboot.createContextContainer()).toBeDefined(); + expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, coreId); + }); + }); + describe('#setup()', () => { test('createContextContainer returns a new container configured with pluginDependencies', () => { const coreId = Symbol(); const service = new ContextService({ coreId } as CoreContext); + + service.preboot({ pluginDependencies: new Map() }); + const setup = service.setup({ pluginDependencies }); expect(setup.createContextContainer()).toBeDefined(); expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, coreId); diff --git a/src/core/server/context/context_service.ts b/src/core/server/context/context_service.ts index 68a99ed3d4156..9e77786c1562c 100644 --- a/src/core/server/context/context_service.ts +++ b/src/core/server/context/context_service.ts @@ -10,6 +10,8 @@ import { PluginOpaqueId } from '../../server'; import { IContextContainer, ContextContainer } from './container'; import { CoreContext } from '../core_context'; +type PrebootDeps = SetupDeps; + interface SetupDeps { pluginDependencies: ReadonlyMap; } @@ -18,7 +20,17 @@ interface SetupDeps { export class ContextService { constructor(private readonly core: CoreContext) {} + public preboot({ pluginDependencies }: PrebootDeps): InternalContextPreboot { + return this.getContextContainerFactory(pluginDependencies); + } + public setup({ pluginDependencies }: SetupDeps): ContextSetup { + return this.getContextContainerFactory(pluginDependencies); + } + + private getContextContainerFactory( + pluginDependencies: ReadonlyMap + ) { return { createContextContainer: () => { return new ContextContainer(pluginDependencies, this.core.coreId); @@ -27,6 +39,9 @@ export class ContextService { } } +/** @internal */ +export type InternalContextPreboot = ContextSetup; + /** * {@inheritdoc IContextContainer} * diff --git a/src/core/server/context/index.ts b/src/core/server/context/index.ts index 84f7ad07da2c3..d12bafdef7a90 100644 --- a/src/core/server/context/index.ts +++ b/src/core/server/context/index.ts @@ -7,7 +7,7 @@ */ export { ContextService } from './context_service'; -export type { ContextSetup } from './context_service'; +export type { InternalContextPreboot, ContextSetup } from './context_service'; export type { IContextContainer, IContextProvider, diff --git a/src/core/server/core_app/core_app.test.ts b/src/core/server/core_app/core_app.test.ts index ad7af3ac8b84d..f6a9b653ec034 100644 --- a/src/core/server/core_app/core_app.test.ts +++ b/src/core/server/core_app/core_app.test.ts @@ -9,10 +9,13 @@ import { registerBundleRoutesMock } from './core_app.test.mocks'; import { mockCoreContext } from '../core_context.mock'; -import { coreMock } from '../mocks'; +import { coreMock, httpServerMock } from '../mocks'; import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; import type { UiPlugins } from '../plugins'; +import { PluginType } from '../plugins'; import { CoreApp } from './core_app'; +import { mockRouter } from '../http/router/router.mock'; +import { RequestHandlerContext } from 'kibana/server'; const emptyPlugins = (): UiPlugins => ({ internal: new Map(), @@ -23,11 +26,23 @@ const emptyPlugins = (): UiPlugins => ({ describe('CoreApp', () => { let coreContext: ReturnType; let coreApp: CoreApp; + let internalCorePreboot: ReturnType; + let prebootHTTPResourcesRegistrar: ReturnType; let internalCoreSetup: ReturnType; let httpResourcesRegistrar: ReturnType; beforeEach(() => { coreContext = mockCoreContext.create(); + + internalCorePreboot = coreMock.createInternalPreboot(); + internalCorePreboot.http.registerRoutes.mockImplementation((path, callback) => + callback(mockRouter.create()) + ); + prebootHTTPResourcesRegistrar = httpResourcesMock.createRegistrar(); + internalCorePreboot.httpResources.createRegistrar.mockReturnValue( + prebootHTTPResourcesRegistrar + ); + internalCoreSetup = coreMock.createInternalSetup(); httpResourcesRegistrar = httpResourcesMock.createRegistrar(); internalCoreSetup.httpResources.createRegistrar.mockReturnValue(httpResourcesRegistrar); @@ -72,6 +87,60 @@ describe('CoreApp', () => { }); }); + describe('#preboot', () => { + let prebootUIPlugins: UiPlugins; + beforeEach(() => { + prebootUIPlugins = emptyPlugins(); + prebootUIPlugins.public.set('some-plugin', { + type: PluginType.preboot, + configPath: 'some-plugin', + id: 'some-plugin', + optionalPlugins: [], + requiredBundles: [], + requiredPlugins: [], + }); + }); + it('calls `registerBundleRoutes` with the correct options', () => { + coreApp.preboot(internalCorePreboot, prebootUIPlugins); + + expect(registerBundleRoutesMock).toHaveBeenCalledTimes(1); + expect(registerBundleRoutesMock).toHaveBeenCalledWith({ + uiPlugins: prebootUIPlugins, + router: expect.any(Object), + packageInfo: coreContext.env.packageInfo, + serverBasePath: internalCorePreboot.http.basePath.serverBasePath, + }); + }); + + it('does not call `registerBundleRoutes` if there are no `preboot` UI plugins', () => { + coreApp.preboot(internalCorePreboot, emptyPlugins()); + + expect(registerBundleRoutesMock).not.toHaveBeenCalled(); + }); + + it('main route handles core app rendering', () => { + coreApp.preboot(internalCorePreboot, prebootUIPlugins); + + expect(prebootHTTPResourcesRegistrar.register).toHaveBeenCalledWith( + { + path: '/{path*}', + validate: expect.any(Object), + }, + expect.any(Function) + ); + + const [[, handler]] = prebootHTTPResourcesRegistrar.register.mock.calls; + const mockResponseFactory = httpResourcesMock.createResponseFactory(); + handler( + ({} as unknown) as RequestHandlerContext, + httpServerMock.createKibanaRequest(), + mockResponseFactory + ); + + expect(mockResponseFactory.renderAnonymousCoreApp).toHaveBeenCalled(); + }); + }); + describe('`/app/{id}/{any*}` route', () => { it('is registered with the correct parameters', () => { coreApp.setup(internalCoreSetup, emptyPlugins()); @@ -89,7 +158,7 @@ describe('CoreApp', () => { }); }); - it('calls `registerBundleRoutes` with the correct options', () => { + it('`setup` calls `registerBundleRoutes` with the correct options', () => { const uiPlugins = emptyPlugins(); coreApp.setup(internalCoreSetup, uiPlugins); diff --git a/src/core/server/core_app/core_app.ts b/src/core/server/core_app/core_app.ts index e728cb0b82475..35a7c57b67610 100644 --- a/src/core/server/core_app/core_app.ts +++ b/src/core/server/core_app/core_app.ts @@ -12,12 +12,25 @@ import { Env } from '@kbn/config'; import { schema } from '@kbn/config-schema'; import { fromRoot } from '@kbn/utils'; -import { InternalCoreSetup } from '../internal_types'; +import { IRouter, IBasePath, IKibanaResponse, KibanaResponseFactory } from '../http'; +import { HttpResources, HttpResourcesServiceToolkit } from '../http_resources'; +import { InternalCorePreboot, InternalCoreSetup } from '../internal_types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { registerBundleRoutes } from './bundle_routes'; import { UiPlugins } from '../plugins'; +/** @internal */ +interface CommonRoutesParams { + router: IRouter; + httpResources: HttpResources; + basePath: IBasePath; + uiPlugins: UiPlugins; + onResourceNotFound: ( + res: HttpResourcesServiceToolkit & KibanaResponseFactory + ) => Promise; +} + /** @internal */ export class CoreApp { private readonly logger: Logger; @@ -28,12 +41,34 @@ export class CoreApp { this.env = core.env; } + preboot(corePreboot: InternalCorePreboot, uiPlugins: UiPlugins) { + this.logger.debug('Prebooting core app.'); + + // We register app-serving routes only if there are `preboot` plugins that may need them. + if (uiPlugins.public.size > 0) { + this.registerPrebootDefaultRoutes(corePreboot, uiPlugins); + this.registerStaticDirs(corePreboot); + } + } + setup(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) { this.logger.debug('Setting up core app.'); this.registerDefaultRoutes(coreSetup, uiPlugins); this.registerStaticDirs(coreSetup); } + private registerPrebootDefaultRoutes(corePreboot: InternalCorePreboot, uiPlugins: UiPlugins) { + corePreboot.http.registerRoutes('', (router) => { + this.registerCommonDefaultRoutes({ + basePath: corePreboot.http.basePath, + httpResources: corePreboot.httpResources.createRegistrar(router), + router, + uiPlugins, + onResourceNotFound: (res) => res.renderAnonymousCoreApp(), + }); + }); + } + private registerDefaultRoutes(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) { const httpSetup = coreSetup.http; const router = httpSetup.createRouter(''); @@ -51,8 +86,55 @@ export class CoreApp { }); }); - // remove trailing slash catch-all - router.get( + this.registerCommonDefaultRoutes({ + basePath: coreSetup.http.basePath, + httpResources: resources, + router, + uiPlugins, + onResourceNotFound: async (res) => res.notFound(), + }); + + resources.register( + { + path: '/app/{id}/{any*}', + validate: false, + options: { + authRequired: true, + }, + }, + async (context, request, response) => { + return response.renderCoreApp(); + } + ); + + const anonymousStatusPage = coreSetup.status.isStatusPageAnonymous(); + resources.register( + { + path: '/status', + validate: false, + options: { + authRequired: !anonymousStatusPage, + }, + }, + async (context, request, response) => { + if (anonymousStatusPage) { + return response.renderAnonymousCoreApp(); + } else { + return response.renderCoreApp(); + } + } + ); + } + + private registerCommonDefaultRoutes({ + router, + basePath, + uiPlugins, + onResourceNotFound, + httpResources, + }: CommonRoutesParams) { + // catch-all route + httpResources.register( { path: '/{path*}', validate: { @@ -66,17 +148,18 @@ export class CoreApp { const { query, params } = req; const { path } = params; if (!path || !path.endsWith('/') || path.startsWith('/')) { - return res.notFound(); + return onResourceNotFound(res); } - const basePath = httpSetup.basePath.get(req); + // remove trailing slash + const requestBasePath = basePath.get(req); let rewrittenPath = path.slice(0, -1); - if (`/${path}`.startsWith(basePath)) { - rewrittenPath = rewrittenPath.substring(basePath.length); + if (`/${path}`.startsWith(requestBasePath)) { + rewrittenPath = rewrittenPath.substring(requestBasePath.length); } const querystring = query ? stringify(query) : undefined; - const url = `${basePath}/${rewrittenPath}${querystring ? `?${querystring}` : ''}`; + const url = `${requestBasePath}/${rewrittenPath}${querystring ? `?${querystring}` : ''}`; return res.redirected({ headers: { @@ -94,45 +177,14 @@ export class CoreApp { router, uiPlugins, packageInfo: this.env.packageInfo, - serverBasePath: coreSetup.http.basePath.serverBasePath, + serverBasePath: basePath.serverBasePath, }); - - resources.register( - { - path: '/app/{id}/{any*}', - validate: false, - options: { - authRequired: true, - }, - }, - async (context, request, response) => { - return response.renderCoreApp(); - } - ); - - const anonymousStatusPage = coreSetup.status.isStatusPageAnonymous(); - resources.register( - { - path: '/status', - validate: false, - options: { - authRequired: !anonymousStatusPage, - }, - }, - async (context, request, response) => { - if (anonymousStatusPage) { - return response.renderAnonymousCoreApp(); - } else { - return response.renderCoreApp(); - } - } - ); } - private registerStaticDirs(coreSetup: InternalCoreSetup) { - coreSetup.http.registerStaticDir('/ui/{path*}', Path.resolve(__dirname, './assets')); + private registerStaticDirs(core: InternalCoreSetup | InternalCorePreboot) { + core.http.registerStaticDir('/ui/{path*}', Path.resolve(__dirname, './assets')); - coreSetup.http.registerStaticDir( + core.http.registerStaticDir( '/node_modules/@kbn/ui-framework/dist/{path*}', fromRoot('node_modules/@kbn/ui-framework/dist') ); diff --git a/src/core/server/core_app/integration_tests/bundle_routes.test.ts b/src/core/server/core_app/integration_tests/bundle_routes.test.ts index 7c50e09b12468..6d37241c2a36f 100644 --- a/src/core/server/core_app/integration_tests/bundle_routes.test.ts +++ b/src/core/server/core_app/integration_tests/bundle_routes.test.ts @@ -26,12 +26,13 @@ describe('bundle routes', () => { let logger: ReturnType; let fileHashCache: FileHashCache; - beforeEach(() => { + beforeEach(async () => { contextSetup = contextServiceMock.createSetupContract(); logger = loggingSystemMock.create(); fileHashCache = new FileHashCache(); server = createHttpServer({ logger }); + await server.preboot({ context: contextServiceMock.createPrebootContract() }); }); afterEach(async () => { diff --git a/src/core/server/core_app/integration_tests/core_app_routes.test.ts b/src/core/server/core_app/integration_tests/core_app_routes.test.ts index faa1c905afa9d..a12e9e7d55188 100644 --- a/src/core/server/core_app/integration_tests/core_app_routes.test.ts +++ b/src/core/server/core_app/integration_tests/core_app_routes.test.ts @@ -20,6 +20,7 @@ describe('Core app routes', () => { }, }); + await root.preboot(); await root.setup(); await root.start(); }); diff --git a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts index dcb623c1ffc74..2ffe3b9d1e4cc 100644 --- a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts +++ b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts @@ -24,6 +24,7 @@ describe('default route provider', () => { }, }); + await root.preboot(); await root.setup(); await root.start(); }); diff --git a/src/core/server/core_app/integration_tests/static_assets.test.ts b/src/core/server/core_app/integration_tests/static_assets.test.ts index 1c7b7db305b7a..86da1d94d3fc6 100644 --- a/src/core/server/core_app/integration_tests/static_assets.test.ts +++ b/src/core/server/core_app/integration_tests/static_assets.test.ts @@ -15,6 +15,7 @@ describe('Platform assets', function () { beforeAll(async function () { root = kbnTestServer.createRoot({ plugins: { initialize: false } }); + await root.preboot(); await root.setup(); await root.start(); }); diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts index 924f1584c5f8f..ed23bf02d23ae 100644 --- a/src/core/server/elasticsearch/client/configure_client.test.ts +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -10,7 +10,11 @@ import { Buffer } from 'buffer'; import { Readable } from 'stream'; import { RequestEvent, errors } from '@elastic/elasticsearch'; -import { TransportRequestParams, RequestBody } from '@elastic/elasticsearch/lib/Transport'; +import type { + TransportRequestOptions, + TransportRequestParams, + RequestBody, +} from '@elastic/elasticsearch/lib/Transport'; import { parseClientOptionsMock, ClientMock } from './configure_client.test.mocks'; import { loggingSystemMock } from '../../logging/logging_system.mock'; @@ -39,12 +43,14 @@ const createApiResponse = ({ headers = {}, warnings = [], params, + requestOptions = {}, }: { body: T; statusCode?: number; headers?: Record; warnings?: string[]; params?: TransportRequestParams; + requestOptions?: TransportRequestOptions; }): RequestEvent => { return { body, @@ -54,6 +60,7 @@ const createApiResponse = ({ meta: { request: { params: params!, + options: requestOptions, } as any, } as any, }; @@ -146,6 +153,7 @@ describe('configureClient', () => { "200 GET /foo?hello=dolly {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", + undefined, ], ] `); @@ -170,6 +178,7 @@ describe('configureClient', () => { "200 GET /foo?hello=dolly {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}", + undefined, ], ] `); @@ -196,6 +205,7 @@ describe('configureClient', () => { "200 GET /foo?hello=dolly [buffer]", + undefined, ], ] `); @@ -222,6 +232,7 @@ describe('configureClient', () => { "200 GET /foo?hello=dolly [stream]", + undefined, ], ] `); @@ -238,6 +249,7 @@ describe('configureClient', () => { Array [ "200 GET /foo?hello=dolly", + undefined, ], ] `); @@ -263,6 +275,7 @@ describe('configureClient', () => { Array [ "200 GET /foo?city=M%C3%BCnich", + undefined, ], ] `); @@ -298,6 +311,7 @@ describe('configureClient', () => { "500 GET /foo?hello=dolly {\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}} [internal server error]: internal server error", + undefined, ], ] `); @@ -313,6 +327,7 @@ describe('configureClient', () => { Array [ Array [ "[TimeoutError]: message", + undefined, ], ] `); @@ -343,6 +358,7 @@ describe('configureClient', () => { Array [ "400 GET /_path?hello=dolly [illegal_argument_exception]: request [/_path] contains unrecognized parameter: [name]", + undefined, ], ] `); @@ -369,6 +385,7 @@ describe('configureClient', () => { Array [ "400 GET /_path [undefined]: Response Error", + undefined, ], ] `); @@ -391,10 +408,67 @@ describe('configureClient', () => { Array [ "400 GET /_path [undefined]: Response Error", + undefined, ], ] `); }); + + it('adds meta information to logs', () => { + const client = configureClient(createFakeConfig(), { logger, type: 'test', scoped: false }); + + let response = createApiResponse({ + statusCode: 400, + headers: {}, + params: { + method: 'GET', + path: '/_path', + }, + requestOptions: { + opaqueId: 'opaque-id', + }, + body: { + error: {}, + }, + }); + client.emit('response', null, response); + + expect(loggingSystemMock.collect(logger).debug[0][1]).toMatchInlineSnapshot(` + Object { + "http": Object { + "request": Object { + "id": "opaque-id", + }, + }, + } + `); + + logger.debug.mockClear(); + + response = createApiResponse({ + statusCode: 400, + headers: {}, + params: { + method: 'GET', + path: '/_path', + }, + requestOptions: { + opaqueId: 'opaque-id', + }, + body: {} as any, + }); + client.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).debug[0][1]).toMatchInlineSnapshot(` + Object { + "http": Object { + "request": Object { + "id": "opaque-id", + }, + }, + } + `); + }); }); }); }); diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index ce4bd6fa2c59b..18c9724c2f8f4 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -98,10 +98,16 @@ function getResponseMessage(event: RequestEvent): string { const addLogging = (client: Client, logger: Logger) => { client.on('response', (error, event) => { if (event) { + const opaqueId = event.meta.request.options.opaqueId; + const meta = opaqueId + ? { + http: { request: { id: event.meta.request.options.opaqueId } }, + } + : undefined; // do not clutter logs if opaqueId is not present if (error) { - logger.debug(getErrorMessage(error, event)); + logger.debug(getErrorMessage(error, event), meta); } else { - logger.debug(getResponseMessage(event)); + logger.debug(getResponseMessage(event), meta); } } }); diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index 089028fc32976..0ccc0f51f6abd 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -19,10 +19,16 @@ import { ElasticsearchClientConfig } from './client'; import { legacyClientMock } from './legacy/mocks'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; -import { InternalElasticsearchServiceSetup, ElasticsearchStatusMeta } from './types'; +import { + InternalElasticsearchServiceSetup, + ElasticsearchStatusMeta, + ElasticsearchServicePreboot, +} from './types'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus, ServiceStatusLevels } from '../status'; +type MockedElasticSearchServicePreboot = jest.Mocked; + export interface MockedElasticSearchServiceSetup { legacy: { config$: BehaviorSubject; @@ -38,6 +44,17 @@ type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup & { >; }; +const createPrebootContractMock = () => { + const prebootContract: MockedElasticSearchServicePreboot = { + config: { hosts: [], credentialsSpecified: false }, + createClient: jest.fn(), + }; + prebootContract.createClient.mockImplementation(() => + elasticsearchClientMock.createCustomClusterClient() + ); + return prebootContract; +}; + const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { legacy: { @@ -73,7 +90,7 @@ const createStartContractMock = () => { return startContract; }; -const createInternalStartContractMock = createStartContractMock; +const createInternalPrebootContractMock = createPrebootContractMock; type MockedInternalElasticSearchServiceSetup = jest.Mocked< InternalElasticsearchServiceSetup & { @@ -102,13 +119,17 @@ const createInternalSetupContractMock = () => { return setupContract; }; +const createInternalStartContractMock = createStartContractMock; + type ElasticsearchServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { + preboot: jest.fn(), setup: jest.fn(), start: jest.fn(), stop: jest.fn(), }; + mocked.preboot.mockResolvedValue(createInternalPrebootContractMock()); mocked.setup.mockResolvedValue(createInternalSetupContractMock()); mocked.start.mockResolvedValueOnce(createInternalStartContractMock()); mocked.stop.mockResolvedValue(); @@ -117,6 +138,8 @@ const createMock = () => { export const elasticsearchServiceMock = { create: createMock, + createInternalPreboot: createInternalPrebootContractMock, + createPreboot: createPrebootContractMock, createInternalSetup: createInternalSetupContractMock, createSetup: createSetupContractMock, createInternalStart: createInternalStartContractMock, diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 791ae2ab7abaa..8932a4c73e1f2 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -16,7 +16,7 @@ import { CoreContext } from '../core_context'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; -import { ElasticsearchConfig } from './elasticsearch_config'; +import { configSchema, ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { elasticsearchServiceMock } from './elasticsearch_service.mock'; import { elasticsearchClientMock } from './client/mocks'; @@ -31,17 +31,6 @@ const setupDeps = { http: httpServiceMock.createInternalSetupContract(), executionContext: executionContextServiceMock.createInternalSetupContract(), }; -configService.atPath.mockReturnValue( - new BehaviorSubject({ - hosts: ['http://1.2.3.4'], - healthCheck: { - delay: duration(10), - }, - ssl: { - verificationMode: 'none', - }, - } as any) -); let env: Env; let coreContext: CoreContext; @@ -51,10 +40,21 @@ let mockClusterClientInstance: ReturnType; - +let mockConfig$: BehaviorSubject; beforeEach(() => { env = Env.createDefault(REPO_ROOT, getEnvOptions()); + mockConfig$ = new BehaviorSubject({ + hosts: ['http://1.2.3.4'], + healthCheck: { + delay: duration(10), + }, + ssl: { + verificationMode: 'none', + }, + }); + configService.atPath.mockReturnValue(mockConfig$); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; elasticsearchService = new ElasticsearchService(coreContext); @@ -69,6 +69,90 @@ beforeEach(() => { afterEach(() => jest.clearAllMocks()); +describe('#preboot', () => { + describe('#config', () => { + it('exposes `hosts`', async () => { + const prebootContract = await elasticsearchService.preboot(); + expect(prebootContract.config).toEqual({ + credentialsSpecified: false, + hosts: ['http://1.2.3.4'], + }); + }); + + it('set `credentialsSpecified` to `true` if `username` is specified', async () => { + mockConfig$.next(configSchema.validate({ username: 'kibana_system' })); + const prebootContract = await elasticsearchService.preboot(); + expect(prebootContract.config.credentialsSpecified).toBe(true); + }); + + it('set `credentialsSpecified` to `true` if `password` is specified', async () => { + mockConfig$.next(configSchema.validate({ password: 'changeme' })); + const prebootContract = await elasticsearchService.preboot(); + expect(prebootContract.config.credentialsSpecified).toBe(true); + }); + + it('set `credentialsSpecified` to `true` if `serviceAccountToken` is specified', async () => { + mockConfig$.next(configSchema.validate({ serviceAccountToken: 'xxxx' })); + const prebootContract = await elasticsearchService.preboot(); + expect(prebootContract.config.credentialsSpecified).toBe(true); + }); + }); + + describe('#createClient', () => { + it('allows to specify config properties', async () => { + const prebootContract = await elasticsearchService.preboot(); + const customConfig = { keepAlive: true }; + const clusterClient = prebootContract.createClient('custom-type', customConfig); + + expect(clusterClient).toBe(mockClusterClientInstance); + + expect(MockClusterClient).toHaveBeenCalledTimes(1); + expect(MockClusterClient.mock.calls[0][0]).toEqual(expect.objectContaining(customConfig)); + }); + + it('creates a new client on each call', async () => { + const prebootContract = await elasticsearchService.preboot(); + + const customConfig = { keepAlive: true }; + + prebootContract.createClient('custom-type', customConfig); + prebootContract.createClient('another-type', customConfig); + + expect(MockClusterClient).toHaveBeenCalledTimes(2); + }); + + it('falls back to elasticsearch default config values if property not specified', async () => { + const prebootContract = await elasticsearchService.preboot(); + + const customConfig = { + hosts: ['http://8.8.8.8'], + logQueries: true, + ssl: { certificate: 'certificate-value' }, + }; + + prebootContract.createClient('some-custom-type', customConfig); + const config = MockClusterClient.mock.calls[0][0]; + + expect(config).toMatchInlineSnapshot(` + Object { + "healthCheckDelay": "PT0.01S", + "hosts": Array [ + "http://8.8.8.8", + ], + "logQueries": true, + "requestHeadersWhitelist": Array [ + undefined, + ], + "ssl": Object { + "certificate": "certificate-value", + "verificationMode": "none", + }, + } + `); + }); + }); +}); + describe('#setup', () => { it('returns legacy Elasticsearch config as a part of the contract', async () => { const setupContract = await elasticsearchService.setup(setupDeps); @@ -249,7 +333,7 @@ describe('#setup', () => { describe('#start', () => { it('throws if called before `setup`', async () => { - expect(() => elasticsearchService.start()).rejects.toMatchInlineSnapshot( + await expect(() => elasticsearchService.start()).rejects.toMatchInlineSnapshot( `[Error: ElasticsearchService needs to be setup before calling start]` ); }); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index deb2d49f70817..f983a8b77fe08 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -18,11 +18,15 @@ import { ILegacyCustomClusterClient, LegacyElasticsearchClientConfig, } from './legacy'; -import { ClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from './client'; +import { ClusterClient, ElasticsearchClientConfig } from './client'; import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; -import type { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; +import type { InternalHttpServiceSetup, GetAuthHeaders } from '../http'; import type { InternalExecutionContextSetup, IExecutionContext } from '../execution_context'; -import { InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart } from './types'; +import { + InternalElasticsearchServicePreboot, + InternalElasticsearchServiceSetup, + InternalElasticsearchServiceStart, +} from './types'; import { pollEsNodesVersion } from './version_check/ensure_es_version'; import { calculateStatus$ } from './status'; @@ -57,6 +61,22 @@ export class ElasticsearchService .pipe(map((rawConfig) => new ElasticsearchConfig(rawConfig))); } + public async preboot(): Promise { + this.log.debug('Prebooting elasticsearch service'); + + const config = await this.config$.pipe(first()).toPromise(); + return { + config: { + hosts: config.hosts, + credentialsSpecified: + config.username !== undefined || + config.password !== undefined || + config.serviceAccountToken !== undefined, + }, + createClient: (type, clientConfig) => this.createClusterClient(type, config, clientConfig), + }; + } + public async setup(deps: SetupDeps): Promise { this.log.debug('Setting up elasticsearch service'); @@ -96,18 +116,9 @@ export class ElasticsearchService } const config = await this.config$.pipe(first()).toPromise(); - - const createClient = ( - type: string, - clientConfig: Partial = {} - ): ICustomClusterClient => { - const finalConfig = merge({}, config, clientConfig); - return this.createClusterClient(type, finalConfig); - }; - return { client: this.client!, - createClient, + createClient: (type, clientConfig) => this.createClusterClient(type, config, clientConfig), legacy: { config$: this.config$, client: this.legacyClient, @@ -127,7 +138,12 @@ export class ElasticsearchService } } - private createClusterClient(type: string, config: ElasticsearchClientConfig) { + private createClusterClient( + type: string, + baseConfig: ElasticsearchConfig, + clientConfig?: Partial + ) { + const config = clientConfig ? merge({}, baseConfig, clientConfig) : baseConfig; return new ClusterClient( config, this.coreContext.logger.get('elasticsearch'), diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 94dc10ff4e863..d97e3331c7cf5 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -11,13 +11,16 @@ export { config, configSchema } from './elasticsearch_config'; export { ElasticsearchConfig } from './elasticsearch_config'; export type { NodesVersionCompatibility } from './version_check/ensure_es_version'; export type { + ElasticsearchServicePreboot, ElasticsearchServiceSetup, ElasticsearchServiceStart, ElasticsearchStatusMeta, + InternalElasticsearchServicePreboot, InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart, FakeRequest, ScopeableRequest, + ElasticsearchConfigPreboot, } from './types'; export * from './legacy'; export type { diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 8bbf665cbc096..375c7015b16d7 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -19,6 +19,43 @@ import { IClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus } from '../status'; +/** + * @public + */ +export interface ElasticsearchServicePreboot { + /** + * A limited set of Elasticsearch configuration entries. + * + * @example + * ```js + * const { hosts, credentialsSpecified } = core.elasticsearch.config; + * ``` + */ + readonly config: Readonly; + + /** + * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. + * + * @param type Unique identifier of the client + * @param clientConfig A config consists of Elasticsearch JS client options and + * valid sub-set of Elasticsearch service config. + * We fill all the missing properties in the `clientConfig` using the default + * Elasticsearch config so that we don't depend on default values set and + * controlled by underlying Elasticsearch JS client. + * We don't run validation against the passed config and expect it to be valid. + * + * @example + * ```js + * const client = elasticsearch.createClient('my-app-name', config); + * const data = await client.asInternalUser.search(); + * ``` + */ + readonly createClient: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; +} + /** * @public */ @@ -77,6 +114,9 @@ export interface ElasticsearchServiceSetup { }; } +/** @internal */ +export type InternalElasticsearchServicePreboot = ElasticsearchServicePreboot; + /** @internal */ export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceSetup { esNodesCompatibility$: Observable; @@ -199,3 +239,21 @@ export interface FakeRequest { * See {@link KibanaRequest}. */ export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; + +/** + * A limited set of Elasticsearch configuration entries exposed to the `preboot` plugins at `setup`. + * + * @public + */ +export interface ElasticsearchConfigPreboot { + /** + * Hosts that the client will connect to. If sniffing is enabled, this list will + * be used as seeds to discover the rest of your cluster. + */ + readonly hosts: string[]; + + /** + * Indicates whether Elasticsearch configuration includes credentials (`username`, `password` or `serviceAccountToken`). + */ + readonly credentialsSpecified: boolean; +} diff --git a/src/core/server/environment/environment_service.mock.ts b/src/core/server/environment/environment_service.mock.ts index 2bc5fa89b8e26..ab620c790a157 100644 --- a/src/core/server/environment/environment_service.mock.ts +++ b/src/core/server/environment/environment_service.mock.ts @@ -7,25 +7,39 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import type { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service'; +import type { + EnvironmentService, + InternalEnvironmentServicePreboot, + InternalEnvironmentServiceSetup, +} from './environment_service'; + +const createPrebootContractMock = () => { + const prebootContract: jest.Mocked = { + instanceUuid: 'uuid', + }; + return prebootContract; +}; const createSetupContractMock = () => { - const setupContract: jest.Mocked = { + const prebootContract: jest.Mocked = { instanceUuid: 'uuid', }; - return setupContract; + return prebootContract; }; type EnvironmentServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { + preboot: jest.fn(), setup: jest.fn(), }; - mocked.setup.mockResolvedValue(createSetupContractMock()); + mocked.preboot.mockResolvedValue(createPrebootContractMock()); + mocked.setup.mockReturnValue(createSetupContractMock()); return mocked; }; export const environmentServiceMock = { create: createMock, + createPrebootContract: createPrebootContractMock, createSetupContract: createSetupContractMock, }; diff --git a/src/core/server/environment/environment_service.test.ts b/src/core/server/environment/environment_service.test.ts index fb3ddaa77b416..34647d090b995 100644 --- a/src/core/server/environment/environment_service.test.ts +++ b/src/core/server/environment/environment_service.test.ts @@ -76,9 +76,9 @@ describe('UuidService', () => { jest.clearAllMocks(); }); - describe('#setup()', () => { + describe('#preboot()', () => { it('calls resolveInstanceUuid with correct parameters', async () => { - await service.setup(); + await service.preboot(); expect(resolveInstanceUuid).toHaveBeenCalledTimes(1); expect(resolveInstanceUuid).toHaveBeenCalledWith({ @@ -89,7 +89,7 @@ describe('UuidService', () => { }); it('calls createDataFolder with correct parameters', async () => { - await service.setup(); + await service.preboot(); expect(createDataFolder).toHaveBeenCalledTimes(1); expect(createDataFolder).toHaveBeenCalledWith({ @@ -99,7 +99,7 @@ describe('UuidService', () => { }); it('calls writePidFile with correct parameters', async () => { - await service.setup(); + await service.preboot(); expect(writePidFile).toHaveBeenCalledTimes(1); expect(writePidFile).toHaveBeenCalledWith({ @@ -109,14 +109,14 @@ describe('UuidService', () => { }); it('returns the uuid resolved from resolveInstanceUuid', async () => { - const setup = await service.setup(); + const preboot = await service.preboot(); - expect(setup.instanceUuid).toEqual('SOME_UUID'); + expect(preboot.instanceUuid).toEqual('SOME_UUID'); }); describe('process warnings', () => { it('logs warnings coming from the process', async () => { - await service.setup(); + await service.preboot(); const warning = new Error('something went wrong'); process.emit('warning', warning); @@ -126,7 +126,7 @@ describe('UuidService', () => { }); it('does not log deprecation warnings', async () => { - await service.setup(); + await service.preboot(); const warning = new Error('something went wrong'); warning.name = 'DeprecationWarning'; @@ -136,4 +136,11 @@ describe('UuidService', () => { }); }); }); + + describe('#setup()', () => { + it('returns the uuid resolved from resolveInstanceUuid', async () => { + await expect(service.preboot()).resolves.toEqual({ instanceUuid: 'SOME_UUID' }); + expect(service.setup()).toEqual({ instanceUuid: 'SOME_UUID' }); + }); + }); }); diff --git a/src/core/server/environment/environment_service.ts b/src/core/server/environment/environment_service.ts index e652622049cfa..f96b616256577 100644 --- a/src/core/server/environment/environment_service.ts +++ b/src/core/server/environment/environment_service.ts @@ -20,13 +20,18 @@ import { writePidFile } from './write_pid_file'; /** * @internal */ -export interface InternalEnvironmentServiceSetup { +export interface InternalEnvironmentServicePreboot { /** * Retrieve the Kibana instance uuid. */ instanceUuid: string; } +/** + * @internal + */ +export type InternalEnvironmentServiceSetup = InternalEnvironmentServicePreboot; + /** @internal */ export class EnvironmentService { private readonly log: Logger; @@ -40,7 +45,9 @@ export class EnvironmentService { this.configService = core.configService; } - public async setup() { + public async preboot() { + // IMPORTANT: This code is based on the assumption that none of the configuration values used + // here is supposed to change during preboot phase and it's safe to read them only once. const [pathConfig, serverConfig, pidConfig] = await Promise.all([ this.configService.atPath(pathConfigDef.path).pipe(take(1)).toPromise(), this.configService.atPath(httpConfigDef.path).pipe(take(1)).toPromise(), @@ -73,4 +80,10 @@ export class EnvironmentService { instanceUuid: this.uuid, }; } + + public setup() { + return { + instanceUuid: this.uuid, + }; + } } diff --git a/src/core/server/environment/index.ts b/src/core/server/environment/index.ts index 01d5097887248..886c0f667fdb4 100644 --- a/src/core/server/environment/index.ts +++ b/src/core/server/environment/index.ts @@ -7,6 +7,9 @@ */ export { EnvironmentService } from './environment_service'; -export type { InternalEnvironmentServiceSetup } from './environment_service'; +export type { + InternalEnvironmentServicePreboot, + InternalEnvironmentServiceSetup, +} from './environment_service'; export { config } from './pid_config'; export type { PidConfigType } from './pid_config'; diff --git a/src/core/server/execution_context/execution_context_container.test.ts b/src/core/server/execution_context/execution_context_container.test.ts index 46a688c8abdf0..e8408d37f40f4 100644 --- a/src/core/server/execution_context/execution_context_container.test.ts +++ b/src/core/server/execution_context/execution_context_container.test.ts @@ -25,7 +25,7 @@ describe('KibanaExecutionContext', () => { }; const value = new ExecutionContextContainer(context).toString(); - expect(value).toMatchInlineSnapshot(`"1234-5678;kibana:test-type:42"`); + expect(value).toMatchInlineSnapshot(`"1234-5678;kibana:test-type:test-name:42"`); }); it('returns a limited representation if optional properties are omitted', () => { @@ -37,6 +37,20 @@ describe('KibanaExecutionContext', () => { expect(value).toMatchInlineSnapshot(`"1234-5678"`); }); + it('returns an escaped string representation of provided execution contextStringified', () => { + const context: KibanaServerExecutionContext = { + id: 'Visualization☺漢字', + type: 'test-type', + name: 'test-name', + requestId: '1234-5678', + }; + + const value = new ExecutionContextContainer(context).toString(); + expect(value).toMatchInlineSnapshot( + `"1234-5678;kibana:test-type:test-name:Visualization%E2%98%BA%E6%BC%A2%E5%AD%97"` + ); + }); + it('trims a string representation of provided execution context if it is bigger max allowed size', () => { expect( new Blob([ diff --git a/src/core/server/execution_context/execution_context_container.ts b/src/core/server/execution_context/execution_context_container.ts index 71bf4bb96e1b0..6c4a5606df152 100644 --- a/src/core/server/execution_context/execution_context_container.ts +++ b/src/core/server/execution_context/execution_context_container.ts @@ -57,7 +57,13 @@ export class ExecutionContextContainer implements IExecutionContextContainer { } toString(): string { const ctx = this.#context; - const contextStringified = ctx.type && ctx.id ? `kibana:${ctx.type}:${ctx.id}` : ''; + const contextStringified = + ctx.type && ctx.id && ctx.name + ? // id may contain non-ASCII symbols + `kibana:${encodeURIComponent(ctx.type)}:${encodeURIComponent( + ctx.name + )}:${encodeURIComponent(ctx.id)}` + : ''; const result = contextStringified ? `${ctx.requestId};${contextStringified}` : ctx.requestId; return enforceMaxLength(result); } diff --git a/src/core/server/execution_context/execution_context_service.test.ts b/src/core/server/execution_context/execution_context_service.test.ts index 9b9ab0f48bacb..0c213429e1951 100644 --- a/src/core/server/execution_context/execution_context_service.test.ts +++ b/src/core/server/execution_context/execution_context_service.test.ts @@ -11,14 +11,17 @@ import { InternalExecutionContextSetup, } from './execution_context_service'; import { mockCoreContext } from '../core_context.mock'; +import { loggingSystemMock } from '../logging/logging_system.mock'; const delay = (ms: number = 100) => new Promise((resolve) => setTimeout(resolve, ms)); describe('ExecutionContextService', () => { describe('setup', () => { let service: InternalExecutionContextSetup; - const core = mockCoreContext.create(); - core.configService.atPath.mockReturnValue(new BehaviorSubject({ enabled: true })); + let core: ReturnType; + beforeEach(() => { + core = mockCoreContext.create(); + core.configService.atPath.mockReturnValue(new BehaviorSubject({ enabled: true })); service = new ExecutionContextService(core).setup(); }); @@ -82,6 +85,19 @@ describe('ExecutionContextService', () => { }, ]); }); + + it('emits context to the logs when "set" is called', async () => { + service.set({ + requestId: '0000', + }); + expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "stored the execution context: {\\"requestId\\":\\"0000\\"}", + ], + ] + `); + }); }); describe('config', () => { diff --git a/src/core/server/execution_context/execution_context_service.ts b/src/core/server/execution_context/execution_context_service.ts index 95a854f84d145..b187283e27e34 100644 --- a/src/core/server/execution_context/execution_context_service.ts +++ b/src/core/server/execution_context/execution_context_service.ts @@ -119,7 +119,7 @@ export class ExecutionContextService // we have to use enterWith since Hapi lifecycle model is built on event emitters. // therefore if we wrapped request handler in asyncLocalStorage.run(), we would lose context in other lifecycles. this.asyncLocalStorage.enterWith(contextContainer); - this.log.trace(`stored the execution context: ${contextContainer.toJSON()}`); + this.log.debug(`stored the execution context: ${JSON.stringify(contextContainer)}`); } private reset() { diff --git a/src/core/server/execution_context/integration_tests/tracing.test.ts b/src/core/server/execution_context/integration_tests/tracing.test.ts index c9de5fb98eb02..ade67d0dd2605 100644 --- a/src/core/server/execution_context/integration_tests/tracing.test.ts +++ b/src/core/server/execution_context/integration_tests/tracing.test.ts @@ -41,6 +41,7 @@ describe('trace', () => { }, }, }); + await root.preboot(); }, 30000); afterEach(async () => { @@ -167,6 +168,7 @@ describe('trace', () => { }, }, }); + await rootExecutionContextDisabled.preboot(); }, 30000); afterEach(async () => { @@ -435,7 +437,7 @@ describe('trace', () => { .expect(200); const header = response.body['x-opaque-id']; - expect(header).toContain('kibana:test-type:42'); + expect(header).toContain('kibana:test-type:test-name:42'); }); it('propagates context to Elasticsearch unscoped client', async () => { @@ -456,7 +458,7 @@ describe('trace', () => { .expect(200); const header = response.body['x-opaque-id']; - expect(header).toContain('kibana:test-type:42'); + expect(header).toContain('kibana:test-type:test-name:42'); }); it('a repeat call overwrites the old context', async () => { @@ -484,7 +486,7 @@ describe('trace', () => { .expect(200); const header = response.body['x-opaque-id']; - expect(header).toContain('kibana:new-type:41'); + expect(header).toContain('kibana:new-type:new-name:41'); }); it('does not affect "x-opaque-id" set by user', async () => { @@ -507,7 +509,7 @@ describe('trace', () => { .expect(200); const header = response.body['x-opaque-id']; - expect(header).toBe('my-opaque-id;kibana:test-type:42'); + expect(header).toBe('my-opaque-id;kibana:test-type:test-name:42'); }); it('does not break on non-ASCII characters within execution context', async () => { @@ -536,7 +538,7 @@ describe('trace', () => { .expect(200); const header = response.body['x-opaque-id']; - expect(header).toBe('my-opaque-id;kibana:test-type:42'); + expect(header).toBe('my-opaque-id;kibana:test-type:test-name:42'); }); }); }); diff --git a/src/core/server/http/__snapshots__/http_service.test.ts.snap b/src/core/server/http/__snapshots__/http_service.test.ts.snap index 04b78a84e818e..0377525c74126 100644 --- a/src/core/server/http/__snapshots__/http_service.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_service.test.ts.snap @@ -8,7 +8,7 @@ Array [ ] `; -exports[`spins up notReady server until started if configured with \`autoListen:true\`: 503 response 1`] = ` +exports[`spins up \`preboot\` server until started if configured with \`autoListen:true\`: 503 response 1`] = ` Object { "body": Array [ Array [ diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index a589bc76d21fc..ef5e151083780 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -12,6 +12,8 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { CspConfig } from '../csp'; import { mockRouter, RouterMock } from './router/router.mock'; import { + InternalHttpServicePreboot, + HttpServicePreboot, InternalHttpServiceSetup, HttpServiceSetup, HttpServiceStart, @@ -31,6 +33,10 @@ import { ExternalUrlConfig } from '../external_url'; type BasePathMocked = jest.Mocked; type AuthMocked = jest.Mocked; +export type HttpServicePrebootMock = jest.Mocked; +export type InternalHttpServicePrebootMock = jest.Mocked< + Omit +> & { basePath: BasePathMocked }; export type HttpServiceSetupMock = jest.Mocked< Omit > & { @@ -72,6 +78,31 @@ const createAuthMock = () => { return mock; }; +const createInternalPrebootContractMock = () => { + const mock: InternalHttpServicePrebootMock = { + registerRoutes: jest.fn(), + // @ts-expect-error tsc cannot infer ContextName and uses never + registerRouteHandlerContext: jest.fn(), + registerStaticDir: jest.fn(), + basePath: createBasePathMock(), + csp: CspConfig.DEFAULT, + externalUrl: ExternalUrlConfig.DEFAULT, + auth: createAuthMock(), + }; + return mock; +}; + +const createPrebootContractMock = () => { + const internalMock = createInternalPrebootContractMock(); + + const mock: HttpServicePrebootMock = { + registerRoutes: internalMock.registerRoutes, + basePath: createBasePathMock(), + }; + + return mock; +}; + const createInternalSetupContractMock = () => { const mock: InternalHttpServiceSetupMock = { // we can mock other hapi server methods when we need it @@ -100,6 +131,7 @@ const createInternalSetupContractMock = () => { auth: createAuthMock(), getAuthHeaders: jest.fn(), getServerInfo: jest.fn(), + registerPrebootRoutes: jest.fn(), }; mock.createCookieSessionStorageFactory.mockResolvedValue(sessionStorageMock.createFactory()); mock.createRouter.mockImplementation(() => mockRouter.create()); @@ -165,11 +197,13 @@ type HttpServiceContract = PublicMethodsOf; const createHttpServiceMock = () => { const mocked: jest.Mocked = { + preboot: jest.fn(), setup: jest.fn(), getStartContract: jest.fn(), start: jest.fn(), stop: jest.fn(), }; + mocked.preboot.mockResolvedValue(createInternalPrebootContractMock()); mocked.setup.mockResolvedValue(createInternalSetupContractMock()); mocked.getStartContract.mockReturnValue(createInternalStartContractMock()); mocked.start.mockResolvedValue(createInternalStartContractMock()); @@ -204,6 +238,8 @@ export const httpServiceMock = { create: createHttpServiceMock, createBasePath: createBasePathMock, createAuth: createAuthMock, + createInternalPrebootContract: createInternalPrebootContractMock, + createPrebootContract: createPrebootContractMock, createInternalSetupContract: createInternalSetupContractMock, createSetupContract: createSetupContractMock, createInternalStartContract: createInternalStartContractMock, diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index d8a7b54275480..8d29e3221a2ca 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -20,7 +20,8 @@ import { loggingSystemMock } from '../logging/logging_system.mock'; import { contextServiceMock } from '../context/context_service.mock'; import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; import { config as cspConfig } from '../csp'; -import { config as externalUrlConfig } from '../external_url'; +import { config as externalUrlConfig, ExternalUrlConfig } from '../external_url'; +import { Router } from './router'; const logger = loggingSystemMock.create(); const env = Env.createDefault(REPO_ROOT, getEnvOptions()); @@ -42,8 +43,12 @@ const createConfigService = (value: Partial = {}) => { configService.setSchema(externalUrlConfig.path, externalUrlConfig.schema); return configService; }; +const contextPreboot = contextServiceMock.createPrebootContract(); const contextSetup = contextServiceMock.createSetupContract(); +const prebootDeps = { + context: contextPreboot, +}; const setupDeps = { context: contextSetup, executionContext: executionContextServiceMock.createInternalSetupContract(), @@ -70,35 +75,40 @@ test('creates and sets up http server', async () => { start: jest.fn(), stop: jest.fn(), }; - const notReadyHttpServer = { + const prebootHttpServer = { isListening: () => false, - setup: jest.fn().mockReturnValue({ server: fakeHapiServer }), + setup: jest.fn().mockReturnValue({ server: fakeHapiServer, registerStaticDir: jest.fn() }), start: jest.fn(), stop: jest.fn(), }; + mockHttpServer.mockImplementationOnce(() => prebootHttpServer); mockHttpServer.mockImplementationOnce(() => httpServer); - mockHttpServer.mockImplementationOnce(() => notReadyHttpServer); const service = new HttpService({ coreId, configService, env, logger }); - expect(mockHttpServer.mock.instances.length).toBe(1); + expect(mockHttpServer.mock.instances.length).toBe(2); expect(httpServer.setup).not.toHaveBeenCalled(); - expect(notReadyHttpServer.setup).not.toHaveBeenCalled(); + expect(prebootHttpServer.setup).not.toHaveBeenCalled(); + + await service.preboot(prebootDeps); + expect(httpServer.setup).not.toHaveBeenCalled(); + expect(httpServer.start).not.toHaveBeenCalled(); + + expect(prebootHttpServer.setup).toHaveBeenCalled(); + expect(prebootHttpServer.start).toHaveBeenCalled(); await service.setup(setupDeps); expect(httpServer.setup).toHaveBeenCalled(); expect(httpServer.start).not.toHaveBeenCalled(); - - expect(notReadyHttpServer.setup).toHaveBeenCalled(); - expect(notReadyHttpServer.start).toHaveBeenCalled(); + expect(prebootHttpServer.stop).not.toHaveBeenCalled(); await service.start(); expect(httpServer.start).toHaveBeenCalled(); - expect(notReadyHttpServer.stop).toHaveBeenCalled(); + expect(prebootHttpServer.stop).toHaveBeenCalled(); }); -test('spins up notReady server until started if configured with `autoListen:true`', async () => { +test('spins up `preboot` server until started if configured with `autoListen:true`', async () => { const configService = createConfigService(); const httpServer = { isListening: () => false, @@ -106,19 +116,19 @@ test('spins up notReady server until started if configured with `autoListen:true start: jest.fn(), stop: jest.fn(), }; - const notReadyHapiServer = { + const prebootHapiServer = { start: jest.fn(), stop: jest.fn(), route: jest.fn(), }; mockHttpServer - .mockImplementationOnce(() => httpServer) .mockImplementationOnce(() => ({ - setup: () => ({ server: notReadyHapiServer }), + setup: () => ({ server: prebootHapiServer, registerStaticDir: jest.fn() }), start: jest.fn(), - stop: jest.fn().mockImplementation(() => notReadyHapiServer.stop()), - })); + stop: jest.fn().mockImplementation(() => prebootHapiServer.stop()), + })) + .mockImplementationOnce(() => httpServer); const service = new HttpService({ coreId, @@ -127,7 +137,7 @@ test('spins up notReady server until started if configured with `autoListen:true logger, }); - await service.setup(setupDeps); + await service.preboot(prebootDeps); const mockResponse: any = { code: jest.fn().mockImplementation(() => mockResponse), @@ -137,7 +147,7 @@ test('spins up notReady server until started if configured with `autoListen:true response: jest.fn().mockReturnValue(mockResponse), }; - const [[{ handler }]] = notReadyHapiServer.route.mock.calls; + const [[{ handler }]] = prebootHapiServer.route.mock.calls; const response503 = await handler(httpServerMock.createRawRequest(), mockResponseToolkit); expect(response503).toBe(mockResponse); expect({ @@ -146,15 +156,25 @@ test('spins up notReady server until started if configured with `autoListen:true header: mockResponse.header.mock.calls, }).toMatchSnapshot('503 response'); + await service.setup(setupDeps); await service.start(); expect(httpServer.start).toBeCalledTimes(1); - expect(notReadyHapiServer.stop).toBeCalledTimes(1); + expect(prebootHapiServer.stop).toBeCalledTimes(1); }); test('logs error if already set up', async () => { const configService = createConfigService(); + mockHttpServer.mockImplementationOnce(() => ({ + setup: () => ({ + server: { start: jest.fn(), stop: jest.fn(), route: jest.fn() }, + registerStaticDir: jest.fn(), + }), + start: noop, + stop: noop, + })); + const httpServer = { isListening: () => true, setup: jest.fn().mockReturnValue({ server: fakeHapiServer }), @@ -165,6 +185,7 @@ test('logs error if already set up', async () => { const service = new HttpService({ coreId, configService, env, logger }); + await service.preboot(prebootDeps); await service.setup(setupDeps); expect(loggingSystemMock.collect(logger).warn).toMatchSnapshot(); @@ -179,29 +200,30 @@ test('stops http server', async () => { start: noop, stop: jest.fn(), }; - const notReadyHttpServer = { + const prebootHttpServer = { isListening: () => false, - setup: jest.fn().mockReturnValue({ server: fakeHapiServer }), + setup: jest.fn().mockReturnValue({ server: fakeHapiServer, registerStaticDir: jest.fn() }), start: noop, stop: jest.fn(), }; + mockHttpServer.mockImplementationOnce(() => prebootHttpServer); mockHttpServer.mockImplementationOnce(() => httpServer); - mockHttpServer.mockImplementationOnce(() => notReadyHttpServer); const service = new HttpService({ coreId, configService, env, logger }); + await service.preboot(prebootDeps); await service.setup(setupDeps); await service.start(); expect(httpServer.stop).toHaveBeenCalledTimes(0); - expect(notReadyHttpServer.stop).toHaveBeenCalledTimes(1); + expect(prebootHttpServer.stop).toHaveBeenCalledTimes(1); await service.stop(); expect(httpServer.stop).toHaveBeenCalledTimes(1); }); -test('stops not ready server if it is running', async () => { +test('stops `preboot` server if it is running', async () => { const configService = createConfigService(); const mockHapiServer = { start: jest.fn(), @@ -210,7 +232,7 @@ test('stops not ready server if it is running', async () => { }; const httpServer = { isListening: () => false, - setup: jest.fn().mockReturnValue({ server: mockHapiServer }), + setup: jest.fn().mockReturnValue({ server: mockHapiServer, registerStaticDir: jest.fn() }), start: noop, stop: jest.fn().mockImplementation(() => mockHapiServer.stop()), }; @@ -218,16 +240,61 @@ test('stops not ready server if it is running', async () => { const service = new HttpService({ coreId, configService, env, logger }); - await service.setup(setupDeps); + await service.preboot(prebootDeps); await service.stop(); expect(mockHapiServer.stop).toHaveBeenCalledTimes(2); }); +test('does not try to stop `preboot` server if it has been already stopped', async () => { + const prebootHttpServer = { + isListening: () => false, + setup: jest.fn().mockReturnValue({ server: fakeHapiServer, registerStaticDir: jest.fn() }), + start: noop, + stop: jest.fn(), + }; + const standardHttpServer = { + isListening: () => false, + setup: jest.fn().mockReturnValue({ server: fakeHapiServer }), + start: noop, + stop: jest.fn(), + }; + + mockHttpServer + .mockImplementationOnce(() => prebootHttpServer) + .mockImplementationOnce(() => standardHttpServer); + + const service = new HttpService({ coreId, configService: createConfigService(), env, logger }); + await service.preboot(prebootDeps); + await service.setup(setupDeps); + + expect(prebootHttpServer.stop).not.toHaveBeenCalled(); + expect(standardHttpServer.stop).not.toHaveBeenCalled(); + + await service.start(); + + expect(prebootHttpServer.stop).toHaveBeenCalledTimes(1); + expect(standardHttpServer.stop).not.toHaveBeenCalled(); + + await service.stop(); + + expect(prebootHttpServer.stop).toHaveBeenCalledTimes(1); + expect(standardHttpServer.stop).toHaveBeenCalledTimes(1); +}); + test('register route handler', async () => { const configService = createConfigService(); + mockHttpServer.mockImplementationOnce(() => ({ + setup: () => ({ + server: { start: jest.fn(), stop: jest.fn(), route: jest.fn() }, + registerStaticDir: jest.fn(), + }), + start: noop, + stop: noop, + })); + const registerRouterMock = jest.fn(); const httpServer = { isListening: () => false, @@ -241,6 +308,7 @@ test('register route handler', async () => { const service = new HttpService({ coreId, configService, env, logger }); + await service.preboot(prebootDeps); const { createRouter } = await service.setup(setupDeps); const router = createRouter('/foo'); @@ -248,10 +316,103 @@ test('register route handler', async () => { expect(registerRouterMock).toHaveBeenLastCalledWith(router); }); +test('register preboot route handler on preboot', async () => { + const registerRouterMock = jest.fn(); + mockHttpServer.mockImplementationOnce(() => ({ + setup: () => ({ + server: { start: jest.fn(), stop: jest.fn(), route: jest.fn() }, + registerStaticDir: jest.fn(), + registerRouterAfterListening: registerRouterMock, + }), + start: noop, + stop: noop, + })); + + const service = new HttpService({ coreId, configService: createConfigService(), env, logger }); + + const registerRoutesMock = jest.fn(); + const { registerRoutes } = await service.preboot(prebootDeps); + registerRoutes('some-path', registerRoutesMock); + + expect(registerRoutesMock).toHaveBeenCalledTimes(1); + expect(registerRoutesMock).toHaveBeenCalledWith(expect.any(Router)); + + const [[router]] = registerRoutesMock.mock.calls; + expect(registerRouterMock).toHaveBeenCalledTimes(1); + expect(registerRouterMock).toHaveBeenCalledWith(router); +}); + +test('register preboot route handler on setup', async () => { + const registerRouterMock = jest.fn(); + mockHttpServer + .mockImplementationOnce(() => ({ + setup: () => ({ + server: { start: jest.fn(), stop: jest.fn(), route: jest.fn() }, + registerStaticDir: jest.fn(), + registerRouterAfterListening: registerRouterMock, + }), + start: noop, + stop: noop, + })) + .mockImplementationOnce(() => ({ setup: () => ({ server: {} }), start: noop, stop: noop })); + + const service = new HttpService({ coreId, configService: createConfigService(), env, logger }); + await service.preboot(prebootDeps); + + const registerRoutesMock = jest.fn(); + const { registerPrebootRoutes } = await service.setup(setupDeps); + registerPrebootRoutes('some-path', registerRoutesMock); + + expect(registerRoutesMock).toHaveBeenCalledTimes(1); + expect(registerRoutesMock).toHaveBeenCalledWith(expect.any(Router)); + + const [[router]] = registerRoutesMock.mock.calls; + expect(registerRouterMock).toHaveBeenCalledTimes(1); + expect(registerRouterMock).toHaveBeenCalledWith(router); +}); + +test('returns `preboot` http server contract on preboot', async () => { + const configService = createConfigService(); + const httpServer = { + server: fakeHapiServer, + registerStaticDir: jest.fn(), + auth: Symbol('auth'), + basePath: Symbol('basePath'), + csp: Symbol('csp'), + }; + + mockHttpServer.mockImplementation(() => ({ + isListening: () => false, + setup: jest.fn().mockReturnValue(httpServer), + start: noop, + stop: noop, + })); + + const service = new HttpService({ coreId, configService, env, logger }); + await expect(service.preboot(prebootDeps)).resolves.toMatchObject({ + auth: httpServer.auth, + basePath: httpServer.basePath, + csp: httpServer.csp, + externalUrl: expect.any(ExternalUrlConfig), + registerRouteHandlerContext: expect.any(Function), + registerRoutes: expect.any(Function), + registerStaticDir: expect.any(Function), + }); +}); + test('returns http server contract on setup', async () => { const configService = createConfigService(); const httpServer = { server: fakeHapiServer, options: { someOption: true } }; + mockHttpServer.mockImplementationOnce(() => ({ + setup: () => ({ + server: { start: jest.fn(), stop: jest.fn(), route: jest.fn() }, + registerStaticDir: jest.fn(), + }), + start: noop, + stop: noop, + })); + mockHttpServer.mockImplementation(() => ({ isListening: () => false, setup: jest.fn().mockReturnValue(httpServer), @@ -260,10 +421,12 @@ test('returns http server contract on setup', async () => { })); const service = new HttpService({ coreId, configService, env, logger }); + await service.preboot(prebootDeps); const setupContract = await service.setup(setupDeps); expect(setupContract).toMatchObject(httpServer); expect(setupContract).toMatchObject({ createRouter: expect.any(Function), + registerPrebootRoutes: expect.any(Function), }); }); @@ -271,6 +434,14 @@ test('does not start http server if configured with `autoListen:false`', async ( const configService = createConfigService({ autoListen: false, }); + mockHttpServer.mockImplementationOnce(() => ({ + setup: () => ({ + server: { start: jest.fn(), stop: jest.fn(), route: jest.fn() }, + registerStaticDir: jest.fn(), + }), + start: noop, + stop: noop, + })); const httpServer = { isListening: () => false, setup: jest.fn().mockReturnValue({}), @@ -286,6 +457,7 @@ test('does not start http server if configured with `autoListen:false`', async ( logger, }); + await service.preboot(prebootDeps); await service.setup(setupDeps); await service.start(); diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 0097aab82b21c..4b9e45e271be2 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -6,21 +6,21 @@ * Side Public License, v 1. */ -import { Observable, Subscription, combineLatest, of } from 'rxjs'; +import { Observable, Subscription, combineLatest } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { pick } from '@kbn/std'; import type { RequestHandlerContext } from 'src/core/server'; import type { InternalExecutionContextSetup } from '../execution_context'; import { CoreService } from '../../types'; -import { Logger, LoggerFactory } from '../logging'; -import { ContextSetup } from '../context'; +import { Logger } from '../logging'; +import { ContextSetup, InternalContextPreboot } from '../context'; import { Env } from '../config'; import { CoreContext } from '../core_context'; import { PluginOpaqueId } from '../plugins'; import { CspConfigType, config as cspConfig } from '../csp'; -import { IRouter, Router } from './router'; +import { Router } from './router'; import { HttpConfig, HttpConfigType, config as httpConfig } from './http_config'; import { HttpServer } from './http_server'; import { HttpsRedirectServer } from './https_redirect_server'; @@ -28,9 +28,9 @@ import { HttpsRedirectServer } from './https_redirect_server'; import { RequestHandlerContextContainer, RequestHandlerContextProvider, + InternalHttpServicePreboot, InternalHttpServiceSetup, InternalHttpServiceStart, - InternalNotReadyHttpServiceSetup, } from './types'; import { registerCoreHandlers } from './lifecycle_handlers'; @@ -40,6 +40,10 @@ import { ExternalUrlConfig, } from '../external_url'; +interface PrebootDeps { + context: InternalContextPreboot; +} + interface SetupDeps { context: ContextSetup; executionContext: InternalExecutionContextSetup; @@ -48,22 +52,22 @@ interface SetupDeps { /** @internal */ export class HttpService implements CoreService { + private readonly prebootServer: HttpServer; + private isPrebootServerStopped = false; private readonly httpServer: HttpServer; private readonly httpsRedirectServer: HttpsRedirectServer; private readonly config$: Observable; private configSubscription?: Subscription; - private readonly logger: LoggerFactory; private readonly log: Logger; private readonly env: Env; - private notReadyServer?: HttpServer; + private internalPreboot?: InternalHttpServicePreboot; private internalSetup?: InternalHttpServiceSetup; private requestHandlerContext?: RequestHandlerContextContainer; constructor(private readonly coreContext: CoreContext) { const { logger, configService, env } = coreContext; - this.logger = logger; this.env = env; this.log = logger.get('http'); this.config$ = combineLatest([ @@ -72,10 +76,63 @@ export class HttpService configService.atPath(externalUrlConfig.path), ]).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl))); const shutdownTimeout$ = this.config$.pipe(map(({ shutdownTimeout }) => shutdownTimeout)); + this.prebootServer = new HttpServer(logger, 'Preboot', shutdownTimeout$); this.httpServer = new HttpServer(logger, 'Kibana', shutdownTimeout$); this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server')); } + public async preboot(deps: PrebootDeps): Promise { + this.log.debug('setting up preboot server'); + const config = await this.config$.pipe(first()).toPromise(); + + const prebootSetup = await this.prebootServer.setup(config); + prebootSetup.server.route({ + path: '/{p*}', + method: '*', + handler: (req, responseToolkit) => { + this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url.href}.`); + + // If server is not ready yet, because plugins or core can perform + // long running tasks (build assets, saved objects migrations etc.) + // we should let client know that and ask to retry after 30 seconds. + return responseToolkit + .response('Kibana server is not ready yet') + .code(503) + .header('Retry-After', '30'); + }, + }); + + if (this.shouldListen(config)) { + this.log.debug('starting preboot server'); + await this.prebootServer.start(); + } + + const prebootServerRequestHandlerContext = deps.context.createContextContainer(); + this.internalPreboot = { + externalUrl: new ExternalUrlConfig(config.externalUrl), + csp: prebootSetup.csp, + basePath: prebootSetup.basePath, + registerStaticDir: prebootSetup.registerStaticDir.bind(prebootSetup), + auth: prebootSetup.auth, + server: prebootSetup.server, + registerRouteHandlerContext: (pluginOpaqueId, contextName, provider) => + prebootServerRequestHandlerContext.registerContext(pluginOpaqueId, contextName, provider), + registerRoutes: (path, registerCallback) => { + const router = new Router( + path, + this.log, + prebootServerRequestHandlerContext.createHandler.bind(null, this.coreContext.coreId) + ); + + registerCallback(router); + + prebootSetup.registerRouterAfterListening(router); + }, + }; + + return this.internalPreboot; + } + public async setup(deps: SetupDeps) { this.requestHandlerContext = deps.context.createContextContainer(); this.configSubscription = this.config$.subscribe(() => { @@ -90,8 +147,6 @@ export class HttpService const config = await this.config$.pipe(first()).toPromise(); - const notReadyServer = await this.setupNotReadyService({ config, context: deps.context }); - const { registerRouter, ...serverContract } = await this.httpServer.setup( config, deps.executionContext @@ -102,8 +157,6 @@ export class HttpService this.internalSetup = { ...serverContract, - notReadyServer, - externalUrl: new ExternalUrlConfig(config.externalUrl), createRouter: ( @@ -124,6 +177,8 @@ export class HttpService contextName: ContextName, provider: RequestHandlerContextProvider ) => this.requestHandlerContext!.registerContext(pluginOpaqueId, contextName, provider), + + registerPrebootRoutes: this.internalPreboot!.registerRoutes, }; return this.internalSetup; @@ -141,11 +196,10 @@ export class HttpService public async start() { const config = await this.config$.pipe(first()).toPromise(); if (this.shouldListen(config)) { - if (this.notReadyServer) { - this.log.debug('stopping NotReady server'); - await this.notReadyServer.stop(); - this.notReadyServer = undefined; - } + this.log.debug('stopping preboot server'); + await this.prebootServer.stop(); + this.isPrebootServerStopped = true; + // If a redirect port is specified, we start an HTTP server at this port and // redirect all requests to the SSL port. if (config.ssl.enabled && config.ssl.redirectHttpFromPort !== undefined) { @@ -169,81 +223,15 @@ export class HttpService } public async stop() { - if (this.configSubscription === undefined) { - return; - } - this.configSubscription?.unsubscribe(); this.configSubscription = undefined; - if (this.notReadyServer) { - await this.notReadyServer.stop(); + if (!this.isPrebootServerStopped) { + this.isPrebootServerStopped = false; + await this.prebootServer.stop(); } + await this.httpServer.stop(); await this.httpsRedirectServer.stop(); } - - private async setupNotReadyService({ - config, - context, - }: { - config: HttpConfig; - context: ContextSetup; - }): Promise { - if (!this.shouldListen(config)) { - return; - } - - const notReadySetup = await this.runNotReadyServer(config); - - // We cannot use the real context container since the core services may not yet be ready - const fakeContext: RequestHandlerContextContainer = new Proxy( - context.createContextContainer(), - { - get: (target, property, receiver) => { - if (property === 'createHandler') { - return Reflect.get(target, property, receiver); - } - throw new Error(`Unexpected access from fake context: ${String(property)}`); - }, - } - ); - - return { - registerRoutes: (path: string, registerCallback: (router: IRouter) => void) => { - const router = new Router( - path, - this.log, - fakeContext.createHandler.bind(null, this.coreContext.coreId) - ); - - registerCallback(router); - notReadySetup.registerRouterAfterListening(router); - }, - }; - } - - private async runNotReadyServer(config: HttpConfig) { - this.log.debug('starting NotReady server'); - this.notReadyServer = new HttpServer(this.logger, 'NotReady', of(config.shutdownTimeout)); - const notReadySetup = await this.notReadyServer.setup(config); - notReadySetup.server.route({ - path: '/{p*}', - method: '*', - handler: (req, responseToolkit) => { - this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url.href}.`); - - // If server is not ready yet, because plugins or core can perform - // long running tasks (build assets, saved objects migrations etc.) - // we should let client know that and ask to retry after 30 seconds. - return responseToolkit - .response('Kibana server is not ready yet') - .code(503) - .header('Retry-After', '30'); - }, - }); - await this.notReadyServer.start(); - - return notReadySetup; - } } diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 84fe5149c89c6..cad5a50dbc505 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -87,6 +87,8 @@ export type { RequestHandlerContextContainer, RequestHandlerContextProvider, HttpAuth, + HttpServicePreboot, + InternalHttpServicePreboot, HttpServiceSetup, InternalHttpServiceSetup, HttpServiceStart, diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 99b63fc73687a..e497f254e0632 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -43,6 +43,7 @@ describe('http service', () => { let root: ReturnType; beforeEach(async () => { root = kbnTestServer.createRoot({ plugins: { initialize: false } }); + await root.preboot(); }, 30000); afterEach(async () => { @@ -189,6 +190,7 @@ describe('http service', () => { let root: ReturnType; beforeEach(async () => { root = kbnTestServer.createRoot({ plugins: { initialize: false } }); + await root.preboot(); }, 30000); afterEach(async () => { @@ -282,6 +284,7 @@ describe('http service', () => { beforeEach(async () => { root = kbnTestServer.createRoot({ plugins: { initialize: false } }); + await root.preboot(); }, 30000); afterEach(async () => { diff --git a/src/core/server/http/integration_tests/http_auth.test.ts b/src/core/server/http/integration_tests/http_auth.test.ts index 0696deb9c07ae..9c923943118a0 100644 --- a/src/core/server/http/integration_tests/http_auth.test.ts +++ b/src/core/server/http/integration_tests/http_auth.test.ts @@ -15,6 +15,7 @@ describe('http auth', () => { beforeEach(async () => { root = kbnTestServer.createRoot({ plugins: { initialize: false } }); + await root.preboot(); }, 30000); afterEach(async () => { diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index da8abe55b6592..e883cd59c8c77 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -29,9 +29,10 @@ const setupDeps = { executionContext: executionContextServiceMock.createInternalSetupContract(), }; -beforeEach(() => { +beforeEach(async () => { logger = loggingSystemMock.create(); server = createHttpServer({ logger }); + await server.preboot({ context: contextServiceMock.createPrebootContract() }); }); afterEach(async () => { diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index 077e2f6e9c485..c633db11edd7a 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -91,6 +91,7 @@ describe('core lifecycle handlers', () => { }); server = createHttpServer({ configService }); + await server.preboot({ context: contextServiceMock.createPrebootContract() }); const serverSetup = await server.setup(setupDeps); router = serverSetup.createRouter('/'); innerServer = serverSetup.server; diff --git a/src/core/server/http/integration_tests/logging.test.ts b/src/core/server/http/integration_tests/logging.test.ts index 62cb699bc49f6..f7eee9580d11a 100644 --- a/src/core/server/http/integration_tests/logging.test.ts +++ b/src/core/server/http/integration_tests/logging.test.ts @@ -28,6 +28,7 @@ describe('request logging', () => { describe('configuration', () => { it('does not log with a default config', async () => { const root = kbnTestServer.createRoot({ plugins: { initialize: false } }); + await root.preboot(); const { http } = await root.setup(); http @@ -69,6 +70,7 @@ describe('request logging', () => { initialize: false, }, }); + await root.preboot(); const { http } = await root.setup(); http @@ -125,6 +127,7 @@ describe('request logging', () => { }); it('handles a GET request', async () => { + await root.preboot(); const { http } = await root.setup(); http @@ -147,6 +150,7 @@ describe('request logging', () => { }); it('handles a POST request', async () => { + await root.preboot(); const { http } = await root.setup(); http.createRouter('/').post( @@ -178,6 +182,7 @@ describe('request logging', () => { }); it('handles an error response', async () => { + await root.preboot(); const { http } = await root.setup(); http @@ -198,6 +203,7 @@ describe('request logging', () => { }); it('handles query strings', async () => { + await root.preboot(); const { http } = await root.setup(); http @@ -216,6 +222,7 @@ describe('request logging', () => { }); it('correctly calculates response payload', async () => { + await root.preboot(); const { http } = await root.setup(); http @@ -234,6 +241,7 @@ describe('request logging', () => { describe('handles request/response headers', () => { it('includes request/response headers in log entry', async () => { + await root.preboot(); const { http } = await root.setup(); http @@ -252,6 +260,7 @@ describe('request logging', () => { }); it('filters sensitive request headers by default', async () => { + await root.preboot(); const { http } = await root.setup(); http.createRouter('/').post( @@ -319,6 +328,7 @@ describe('request logging', () => { initialize: false, }, }); + await root.preboot(); const { http } = await root.setup(); http.createRouter('/').post( @@ -351,6 +361,7 @@ describe('request logging', () => { }); it('filters sensitive response headers by defaut', async () => { + await root.preboot(); const { http } = await root.setup(); http.createRouter('/').post( @@ -416,6 +427,7 @@ describe('request logging', () => { initialize: false, }, }); + await root.preboot(); const { http } = await root.setup(); http.createRouter('/').post( @@ -449,6 +461,7 @@ describe('request logging', () => { }); it('handles user agent', async () => { + await root.preboot(); const { http } = await root.setup(); http diff --git a/src/core/server/http/integration_tests/preboot.test.ts b/src/core/server/http/integration_tests/preboot.test.ts new file mode 100644 index 0000000000000..7c2118bea725b --- /dev/null +++ b/src/core/server/http/integration_tests/preboot.test.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import supertest from 'supertest'; + +import { contextServiceMock } from '../../context/context_service.mock'; +import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { createHttpServer } from '../test_utils'; +import { HttpService } from '../http_service'; + +let server: HttpService; +const prebootDeps = { + context: contextServiceMock.createPrebootContract(), +}; +const setupDeps = { + context: contextServiceMock.createSetupContract(), + executionContext: executionContextServiceMock.createInternalSetupContract(), +}; + +beforeEach(async () => { + server = createHttpServer({ logger: loggingSystemMock.create() }); +}); + +afterEach(async () => { + await server.stop(); +}); + +describe('Preboot HTTP server', () => { + it('accepts requests before `setup`', async () => { + const { server: innerPrebootServer, registerRoutes } = await server.preboot(prebootDeps); + registerRoutes('', (router) => { + router.get({ path: '/preboot-get', validate: false }, (context, req, res) => + res.ok({ body: 'hello-get' }) + ); + router.post({ path: '/preboot-post', validate: false }, (context, req, res) => + res.ok({ body: 'hello-post' }) + ); + }); + + // Preboot routes should work now. + await supertest(innerPrebootServer.listener).get('/preboot-get').expect(200, 'hello-get'); + await supertest(innerPrebootServer.listener).post('/preboot-post').expect(200, 'hello-post'); + + // All non-preboot routes should get `503` (e.g. if client tries to access any standard API). + await supertest(innerPrebootServer.listener) + .get('/standard-get') + .expect(503, 'Kibana server is not ready yet'); + await supertest(innerPrebootServer.listener) + .post('/standard-post') + .expect(503, 'Kibana server is not ready yet'); + }); + + it('accepts requests after `setup`, but before `start`', async () => { + const { server: innerPrebootServer, registerRoutes } = await server.preboot(prebootDeps); + registerRoutes('', (router) => { + router.get({ path: '/preboot-get', validate: false }, (context, req, res) => + res.ok({ body: 'hello-get' }) + ); + router.post({ path: '/preboot-post', validate: false }, (context, req, res) => + res.ok({ body: 'hello-post' }) + ); + }); + + const { createRouter, server: innerStandardServer } = await server.setup(setupDeps); + const standardRouter = createRouter(''); + standardRouter.get({ path: '/standard-get', validate: false }, (context, req, res) => + res.ok({ body: 'hello-get' }) + ); + standardRouter.post({ path: '/standard-post', validate: false }, (context, req, res) => + res.ok({ body: 'hello-post' }) + ); + + // Preboot routes should still work. + await supertest(innerPrebootServer.listener).get('/preboot-get').expect(200, 'hello-get'); + await supertest(innerPrebootServer.listener).post('/preboot-post').expect(200, 'hello-post'); + + // All non-preboot routes should still get `503` (e.g. if client tries to access any standard API). + await supertest(innerPrebootServer.listener) + .get('/standard-get') + .expect(503, 'Kibana server is not ready yet'); + await supertest(innerPrebootServer.listener) + .post('/standard-post') + .expect(503, 'Kibana server is not ready yet'); + + // Standard HTTP server isn't functional yet. + await supertest(innerStandardServer.listener) + .get('/standard-get') + .expect(404, { statusCode: 404, error: 'Not Found', message: 'Not Found' }); + await supertest(innerStandardServer.listener) + .post('/standard-post') + .expect(404, { statusCode: 404, error: 'Not Found', message: 'Not Found' }); + }); + + it('is not available after `start`', async () => { + const { server: innerPrebootServer, registerRoutes } = await server.preboot(prebootDeps); + registerRoutes('', (router) => { + router.get({ path: '/preboot-get', validate: false }, (context, req, res) => + res.ok({ body: 'hello-get' }) + ); + router.post({ path: '/preboot-post', validate: false }, (context, req, res) => + res.ok({ body: 'hello-post' }) + ); + }); + + const { createRouter, server: innerStandardServer } = await server.setup(setupDeps); + const standardRouter = createRouter(''); + standardRouter.get({ path: '/standard-get', validate: false }, (context, req, res) => + res.ok({ body: 'hello-get' }) + ); + standardRouter.post({ path: '/standard-post', validate: false }, (context, req, res) => + res.ok({ body: 'hello-post' }) + ); + + await server.start(); + + // Preboot routes should no longer work. + await supertest(innerPrebootServer.listener).get('/preboot-get').expect(503, { + statusCode: 503, + error: 'Service Unavailable', + message: 'Kibana is shutting down and not accepting new incoming requests', + }); + await supertest(innerPrebootServer.listener).post('/preboot-post').expect(503, { + statusCode: 503, + error: 'Service Unavailable', + message: 'Kibana is shutting down and not accepting new incoming requests', + }); + + // Preboot routes should simply become unknown routes for the standard server. + await supertest(innerStandardServer.listener) + .get('/preboot-get') + .expect(404, { statusCode: 404, error: 'Not Found', message: 'Not Found' }); + await supertest(innerStandardServer.listener) + .post('/preboot-post') + .expect(404, { statusCode: 404, error: 'Not Found', message: 'Not Found' }); + + // All non-preboot routes should finally function as expected (e.g. if client tries to access any standard API). + await supertest(innerStandardServer.listener).get('/standard-get').expect(200, 'hello-get'); + await supertest(innerStandardServer.listener).post('/standard-post').expect(200, 'hello-post'); + }); +}); diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index ecacbf0bfa0c2..0a30bfac85f5d 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -30,10 +30,11 @@ const setupDeps = { executionContext: executionContextServiceMock.createInternalSetupContract(), }; -beforeEach(() => { +beforeEach(async () => { logger = loggingSystemMock.create(); server = createHttpServer({ logger }); + await server.preboot({ context: contextServiceMock.createPrebootContract() }); }); afterEach(async () => { diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 1b2b0b966d3a2..5bea371d479ae 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -28,9 +28,10 @@ const setupDeps = { executionContext: executionContextServiceMock.createInternalSetupContract(), }; -beforeEach(() => { +beforeEach(async () => { logger = loggingSystemMock.create(); server = createHttpServer({ logger }); + await server.preboot({ context: contextServiceMock.createPrebootContract() }); }); afterEach(async () => { diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index bbd296d6b1831..7353f48b47194 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -56,6 +56,109 @@ export interface HttpAuth { isAuthenticated: IsAuthenticated; } +/** + * Kibana HTTP Service provides an abstraction to work with the HTTP stack at the `preboot` stage. This functionality + * allows Kibana to serve user requests even before Kibana becomes fully operational. Only Core and `preboot` plugins + * can define HTTP routes at this stage. + * + * @example + * To handle an incoming request in your preboot plugin you should: + * - Use `@kbn/config-schema` package to create a schema to validate the request `params`, `query`, and `body`. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error without calling the route's handler. + * To opt out of validating the request, specify `false`. + * ```ts + * import { schema, TypeOf } from '@kbn/config-schema'; + * const validate = { + * params: schema.object({ + * id: schema.string(), + * }), + * }; + * ``` + * + * - Declare a function to respond to incoming request. + * The function will receive `request` object containing request details: url, headers, matched route, as well as validated `params`, `query`, `body`. + * And `response` object instructing HTTP server to create HTTP response with information sent back to the client as the response body, headers, and HTTP status. + * Any exception raised during the handler call will generate `500 Server error` response and log error details for further investigation. See below for returning custom error responses. + * ```ts + * const handler = async (context: RequestHandlerContext, request: KibanaRequest, response: ResponseFactory) => { + * const data = await findObject(request.params.id); + * // creates a command to respond with 'not found' error + * if (!data) { + * return response.notFound(); + * } + * // creates a command to send found data to the client and set response headers + * return response.ok({ + * body: data, + * headers: { 'content-type': 'application/json' } + * }); + * } + * ``` + * * - Acquire `preboot` {@link IRouter} instance and register route handler for GET request to 'path/{id}' path. + * ```ts + * import { schema, TypeOf } from '@kbn/config-schema'; + * + * const validate = { + * params: schema.object({ + * id: schema.string(), + * }), + * }; + * + * httpPreboot.registerRoutes('my-plugin', (router) => { + * router.get({ path: 'path/{id}', validate }, async (context, request, response) => { + * const data = await findObject(request.params.id); + * if (!data) { + * return response.notFound(); + * } + * return response.ok({ + * body: data, + * headers: { 'content-type': 'application/json' } + * }); + * }); + * }); + * ``` + * @public + */ +export interface HttpServicePreboot { + /** + * Provides ability to acquire `preboot` {@link IRouter} instance for a particular top-level path and register handler + * functions for any number of nested routes. + * + * @remarks + * Each route can have only one handler function, which is executed when the route is matched. + * See the {@link IRouter} documentation for more information. + * + * @example + * ```ts + * registerRoutes('my-plugin', (router) => { + * // handler is called when '/my-plugin/path' resource is requested with `GET` method + * router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + * }); + * ``` + * @public + */ + registerRoutes(path: string, callback: (router: IRouter) => void): void; + + /** + * Access or manipulate the Kibana base path + * See {@link IBasePath}. + */ + basePath: IBasePath; +} + +/** @internal */ +export interface InternalHttpServicePreboot + extends Pick< + InternalHttpServiceSetup, + | 'auth' + | 'csp' + | 'basePath' + | 'externalUrl' + | 'registerStaticDir' + | 'registerRouteHandlerContext' + | 'server' + > { + registerRoutes(path: string, callback: (router: IRouter) => void): void; +} + /** * Kibana HTTP Service provides own abstraction for work with HTTP stack. * Plugins don't have direct access to `hapi` server and its primitives anymore. Moreover, @@ -277,11 +380,6 @@ export interface HttpServiceSetup { getServerInfo: () => HttpServerInfo; } -/** @internal */ -export interface InternalNotReadyHttpServiceSetup { - registerRoutes(path: string, callback: (router: IRouter) => void): void; -} - /** @internal */ export interface InternalHttpServiceSetup extends Omit { @@ -303,7 +401,7 @@ export interface InternalHttpServiceSetup contextName: ContextName, provider: RequestHandlerContextProvider ) => RequestHandlerContextContainer; - notReadyServer?: InternalNotReadyHttpServiceSetup; + registerPrebootRoutes(path: string, callback: (router: IRouter) => void): void; } /** @public */ diff --git a/src/core/server/http_resources/http_resources_service.mock.ts b/src/core/server/http_resources/http_resources_service.mock.ts index 3a94de15d14b9..a2ca0aa246582 100644 --- a/src/core/server/http_resources/http_resources_service.mock.ts +++ b/src/core/server/http_resources/http_resources_service.mock.ts @@ -13,12 +13,16 @@ const createHttpResourcesMock = (): jest.Mocked => ({ register: jest.fn(), }); -function createInternalHttpResourcesSetup() { +function createInternalHttpResourcesPreboot() { return { createRegistrar: jest.fn(() => createHttpResourcesMock()), }; } +function createInternalHttpResourcesSetup() { + return createInternalHttpResourcesPreboot(); +} + function createHttpResourcesResponseFactory() { const mocked: jest.Mocked = { renderCoreApp: jest.fn(), @@ -35,6 +39,7 @@ function createHttpResourcesResponseFactory() { export const httpResourcesMock = { createRegistrar: createHttpResourcesMock, + createPrebootContract: createInternalHttpResourcesPreboot, createSetupContract: createInternalHttpResourcesSetup, createResponseFactory: createHttpResourcesResponseFactory, }; diff --git a/src/core/server/http_resources/http_resources_service.test.ts b/src/core/server/http_resources/http_resources_service.test.ts index 8b24e05fc5bf4..33ee6cc4e3748 100644 --- a/src/core/server/http_resources/http_resources_service.test.ts +++ b/src/core/server/http_resources/http_resources_service.test.ts @@ -15,13 +15,15 @@ import { mockCoreContext } from '../core_context.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { httpServerMock } from '../http/http_server.mocks'; import { renderingMock } from '../rendering/rendering_service.mock'; -import { HttpResourcesService, SetupDeps } from './http_resources_service'; +import { HttpResourcesService, PrebootDeps, SetupDeps } from './http_resources_service'; import { httpResourcesMock } from './http_resources_service.mock'; +import { HttpResources } from 'kibana/server'; const coreContext = mockCoreContext.create(); describe('HttpResources service', () => { let service: HttpResourcesService; + let prebootDeps: PrebootDeps; let setupDeps: SetupDeps; let router: jest.Mocked; const kibanaRequest = httpServerMock.createKibanaRequest(); @@ -34,6 +36,10 @@ describe('HttpResources service', () => { describe('#createRegistrar', () => { beforeEach(() => { + prebootDeps = { + http: httpServiceMock.createInternalPrebootContract(), + rendering: renderingMock.createPrebootContract(), + }; setupDeps = { http: httpServiceMock.createInternalSetupContract(), rendering: renderingMock.createSetupContract(), @@ -42,221 +48,228 @@ describe('HttpResources service', () => { router = httpServiceMock.createRouter(); }); - describe('register', () => { - describe('renderCoreApp', () => { - it('formats successful response', async () => { - const routeConfig: RouteConfig = { path: '/', validate: false }; - const { createRegistrar } = await service.setup(setupDeps); - const { register } = createRegistrar(router); - register(routeConfig, async (ctx, req, res) => { - return res.renderCoreApp(); - }); - const [[, routeHandler]] = router.get.mock.calls; - - const responseFactory = httpResourcesMock.createResponseFactory(); - await routeHandler(context, kibanaRequest, responseFactory); - expect(setupDeps.rendering.render).toHaveBeenCalledWith( - kibanaRequest, - context.core.uiSettings.client, - { - includeUserSettings: true, - vars: { - apmConfig, - }, - } - ); + function runRegisterTestSuite( + name: string, + initializer: () => Promise, + getDeps: () => PrebootDeps | SetupDeps + ) { + describe(`${name} register`, () => { + const routeConfig: RouteConfig = { path: '/', validate: false }; + let register: HttpResources['register']; + beforeEach(async () => { + register = await initializer(); }); - it('can attach headers, except the CSP header', async () => { - const routeConfig: RouteConfig = { path: '/', validate: false }; - const { createRegistrar } = await service.setup(setupDeps); - const { register } = createRegistrar(router); - register(routeConfig, async (ctx, req, res) => { - return res.renderCoreApp({ - headers: { - 'content-security-policy': "script-src 'unsafe-eval'", - 'x-kibana': '42', - }, + describe('renderCoreApp', () => { + it('formats successful response', async () => { + register(routeConfig, async (ctx, req, res) => { + return res.renderCoreApp(); }); - }); + const [[, routeHandler]] = router.get.mock.calls; - const [[, routeHandler]] = router.get.mock.calls; + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(getDeps().rendering.render).toHaveBeenCalledWith( + kibanaRequest, + context.core.uiSettings.client, + { + includeUserSettings: true, + vars: { + apmConfig, + }, + } + ); + }); - const responseFactory = httpResourcesMock.createResponseFactory(); - await routeHandler(context, kibanaRequest, responseFactory); + it('can attach headers, except the CSP header', async () => { + register(routeConfig, async (ctx, req, res) => { + return res.renderCoreApp({ + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); - expect(responseFactory.ok).toHaveBeenCalledWith({ - body: '', - headers: { - 'x-kibana': '42', - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, - }); - }); - }); - describe('renderAnonymousCoreApp', () => { - it('formats successful response', async () => { - const routeConfig: RouteConfig = { path: '/', validate: false }; - const { createRegistrar } = await service.setup(setupDeps); - const { register } = createRegistrar(router); - register(routeConfig, async (ctx, req, res) => { - return res.renderAnonymousCoreApp(); - }); - const [[, routeHandler]] = router.get.mock.calls; + const [[, routeHandler]] = router.get.mock.calls; - const responseFactory = httpResourcesMock.createResponseFactory(); - await routeHandler(context, kibanaRequest, responseFactory); - expect(setupDeps.rendering.render).toHaveBeenCalledWith( - kibanaRequest, - context.core.uiSettings.client, - { - includeUserSettings: false, - vars: { - apmConfig, - }, - } - ); - }); + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); - it('can attach headers, except the CSP header', async () => { - const routeConfig: RouteConfig = { path: '/', validate: false }; - const { createRegistrar } = await service.setup(setupDeps); - const { register } = createRegistrar(router); - register(routeConfig, async (ctx, req, res) => { - return res.renderAnonymousCoreApp({ + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: '', headers: { - 'content-security-policy': "script-src 'unsafe-eval'", 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", }, }); }); + }); + describe('renderAnonymousCoreApp', () => { + it('formats successful response', async () => { + register(routeConfig, async (ctx, req, res) => { + return res.renderAnonymousCoreApp(); + }); + const [[, routeHandler]] = router.get.mock.calls; - const [[, routeHandler]] = router.get.mock.calls; + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(getDeps().rendering.render).toHaveBeenCalledWith( + kibanaRequest, + context.core.uiSettings.client, + { + includeUserSettings: false, + vars: { + apmConfig, + }, + } + ); + }); - const responseFactory = httpResourcesMock.createResponseFactory(); - await routeHandler(context, kibanaRequest, responseFactory); + it('can attach headers, except the CSP header', async () => { + register(routeConfig, async (ctx, req, res) => { + return res.renderAnonymousCoreApp({ + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); - expect(responseFactory.ok).toHaveBeenCalledWith({ - body: '', - headers: { - 'x-kibana': '42', - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, - }); - }); - }); - describe('renderHtml', () => { - it('formats successful response', async () => { - const htmlBody = ''; - const routeConfig: RouteConfig = { path: '/', validate: false }; - const { createRegistrar } = await service.setup(setupDeps); - const { register } = createRegistrar(router); - register(routeConfig, async (ctx, req, res) => { - return res.renderHtml({ body: htmlBody }); - }); - const [[, routeHandler]] = router.get.mock.calls; + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); - const responseFactory = httpResourcesMock.createResponseFactory(); - await routeHandler(context, kibanaRequest, responseFactory); - expect(responseFactory.ok).toHaveBeenCalledWith({ - body: htmlBody, - headers: { - 'content-type': 'text/html', - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: '', + headers: { + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); }); }); + describe('renderHtml', () => { + it('formats successful response', async () => { + const htmlBody = ''; + register(routeConfig, async (ctx, req, res) => { + return res.renderHtml({ body: htmlBody }); + }); + const [[, routeHandler]] = router.get.mock.calls; - it('can attach headers, except the CSP & "content-type" headers', async () => { - const htmlBody = ''; - const routeConfig: RouteConfig = { path: '/', validate: false }; - const { createRegistrar } = await service.setup(setupDeps); - const { register } = createRegistrar(router); - register(routeConfig, async (ctx, req, res) => { - return res.renderHtml({ + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(responseFactory.ok).toHaveBeenCalledWith({ body: htmlBody, headers: { - 'content-type': 'text/html5', - 'content-security-policy': "script-src 'unsafe-eval'", - 'x-kibana': '42', + 'content-type': 'text/html', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", }, }); }); - const [[, routeHandler]] = router.get.mock.calls; + it('can attach headers, except the CSP & "content-type" headers', async () => { + const htmlBody = ''; + register(routeConfig, async (ctx, req, res) => { + return res.renderHtml({ + body: htmlBody, + headers: { + 'content-type': 'text/html5', + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); - const responseFactory = httpResourcesMock.createResponseFactory(); - await routeHandler(context, kibanaRequest, responseFactory); + const [[, routeHandler]] = router.get.mock.calls; - expect(responseFactory.ok).toHaveBeenCalledWith({ - body: htmlBody, - headers: { - 'content-type': 'text/html', - 'x-kibana': '42', - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, - }); - }); - }); - describe('renderJs', () => { - it('formats successful response', async () => { - const jsBody = 'alert(1);'; - const routeConfig: RouteConfig = { path: '/', validate: false }; - const { createRegistrar } = await service.setup(setupDeps); - const { register } = createRegistrar(router); - register(routeConfig, async (ctx, req, res) => { - return res.renderJs({ body: jsBody }); - }); - const [[, routeHandler]] = router.get.mock.calls; + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); - const responseFactory = httpResourcesMock.createResponseFactory(); - await routeHandler(context, kibanaRequest, responseFactory); - expect(responseFactory.ok).toHaveBeenCalledWith({ - body: jsBody, - headers: { - 'content-type': 'text/javascript', - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: htmlBody, + headers: { + 'content-type': 'text/html', + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); }); }); + describe('renderJs', () => { + it('formats successful response', async () => { + const jsBody = 'alert(1);'; + register(routeConfig, async (ctx, req, res) => { + return res.renderJs({ body: jsBody }); + }); + const [[, routeHandler]] = router.get.mock.calls; - it('can attach headers, except the CSP & "content-type" headers', async () => { - const jsBody = 'alert(1);'; - const routeConfig: RouteConfig = { path: '/', validate: false }; - const { createRegistrar } = await service.setup(setupDeps); - const { register } = createRegistrar(router); - register(routeConfig, async (ctx, req, res) => { - return res.renderJs({ + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(responseFactory.ok).toHaveBeenCalledWith({ body: jsBody, headers: { - 'content-type': 'text/html', - 'content-security-policy': "script-src 'unsafe-eval'", - 'x-kibana': '42', + 'content-type': 'text/javascript', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", }, }); }); - const [[, routeHandler]] = router.get.mock.calls; + it('can attach headers, except the CSP & "content-type" headers', async () => { + const jsBody = 'alert(1);'; + register(routeConfig, async (ctx, req, res) => { + return res.renderJs({ + body: jsBody, + headers: { + 'content-type': 'text/html', + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); + + const [[, routeHandler]] = router.get.mock.calls; - const responseFactory = httpResourcesMock.createResponseFactory(); - await routeHandler(context, kibanaRequest, responseFactory); + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); - expect(responseFactory.ok).toHaveBeenCalledWith({ - body: jsBody, - headers: { - 'content-type': 'text/javascript', - 'x-kibana': '42', - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: jsBody, + headers: { + 'content-type': 'text/javascript', + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); }); }); }); - }); + } + + runRegisterTestSuite( + '#preboot', + async () => { + const { createRegistrar } = await service.preboot(prebootDeps); + return createRegistrar(router).register; + }, + () => prebootDeps + ); + + runRegisterTestSuite( + '#setup', + async () => { + await service.preboot(prebootDeps); + const { createRegistrar } = await service.setup(setupDeps); + return createRegistrar(router).register; + }, + () => setupDeps + ); }); }); diff --git a/src/core/server/http_resources/http_resources_service.ts b/src/core/server/http_resources/http_resources_service.ts index 44caa456e9955..6c295b152af3c 100644 --- a/src/core/server/http_resources/http_resources_service.ts +++ b/src/core/server/http_resources/http_resources_service.ts @@ -15,10 +15,11 @@ import { InternalHttpServiceSetup, KibanaRequest, KibanaResponseFactory, + InternalHttpServicePreboot, } from '../http'; import { Logger } from '../logging'; -import { InternalRenderingServiceSetup } from '../rendering'; +import { InternalRenderingServicePreboot, InternalRenderingServiceSetup } from '../rendering'; import { CoreService } from '../../types'; import { @@ -31,6 +32,11 @@ import { } from './types'; import { getApmConfig } from './get_apm_config'; +export interface PrebootDeps { + http: InternalHttpServicePreboot; + rendering: InternalRenderingServicePreboot; +} + export interface SetupDeps { http: InternalHttpServiceSetup; rendering: InternalRenderingServiceSetup; @@ -43,6 +49,13 @@ export class HttpResourcesService implements CoreService( route: RouteConfig, @@ -71,7 +84,7 @@ export class HttpResourcesService implements CoreService { }, plugins: { initialize: false }, }); + await root.preboot(); }, 30000); afterEach(async () => { diff --git a/src/core/server/http_resources/types.ts b/src/core/server/http_resources/types.ts index 3333574038ec7..1ec02272d151f 100644 --- a/src/core/server/http_resources/types.ts +++ b/src/core/server/http_resources/types.ts @@ -84,10 +84,16 @@ export type HttpResourcesRequestHandler< * Allows to configure HTTP response parameters * @internal */ -export interface InternalHttpResourcesSetup { +export interface InternalHttpResourcesPreboot { createRegistrar(router: IRouter): HttpResources; } +/** + * Allows to configure HTTP response parameters + * @internal + */ +export type InternalHttpResourcesSetup = InternalHttpResourcesPreboot; + /** * HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. * Provides API allowing plug-ins to respond with: diff --git a/src/core/server/i18n/i18n_service.mock.ts b/src/core/server/i18n/i18n_service.mock.ts index 29859e95b63b2..a199acd00eff5 100644 --- a/src/core/server/i18n/i18n_service.mock.ts +++ b/src/core/server/i18n/i18n_service.mock.ts @@ -25,6 +25,7 @@ type I18nServiceContract = PublicMethodsOf; const createMock = () => { const mock: jest.Mocked = { + preboot: jest.fn(), setup: jest.fn(), }; diff --git a/src/core/server/i18n/i18n_service.test.ts b/src/core/server/i18n/i18n_service.test.ts index 913d5ee4aaa99..ad87b371aca33 100644 --- a/src/core/server/i18n/i18n_service.test.ts +++ b/src/core/server/i18n/i18n_service.test.ts @@ -17,7 +17,7 @@ import { I18nService } from './i18n_service'; import { configServiceMock } from '../config/mocks'; import { mockCoreContext } from '../core_context.mock'; -import { httpServiceMock } from '../http/http_service.mock'; +import { httpServiceMock } from '../mocks'; const getConfigService = (locale = 'en') => { const configService = configServiceMock.create(); @@ -35,7 +35,8 @@ const getConfigService = (locale = 'en') => { describe('I18nService', () => { let service: I18nService; let configService: ReturnType; - let http: ReturnType; + let httpPreboot: ReturnType; + let httpSetup: ReturnType; beforeEach(() => { jest.clearAllMocks(); @@ -44,15 +45,60 @@ describe('I18nService', () => { const coreContext = mockCoreContext.create({ configService }); service = new I18nService(coreContext); - http = httpServiceMock.createInternalSetupContract(); + httpPreboot = httpServiceMock.createInternalPrebootContract(); + httpPreboot.registerRoutes.mockImplementation((type, callback) => + callback(httpServiceMock.createRouter()) + ); + httpSetup = httpServiceMock.createInternalSetupContract(); + }); + + describe('#preboot', () => { + it('calls `getKibanaTranslationFiles` with the correct parameters', async () => { + getKibanaTranslationFilesMock.mockResolvedValue([]); + + const pluginPaths = ['/pathA', '/pathB']; + await service.preboot({ pluginPaths, http: httpPreboot }); + + expect(getKibanaTranslationFilesMock).toHaveBeenCalledTimes(1); + expect(getKibanaTranslationFilesMock).toHaveBeenCalledWith('en', pluginPaths); + }); + + it('calls `initTranslations` with the correct parameters', async () => { + const translationFiles = ['/path/to/file', 'path/to/another/file']; + getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); + + await service.preboot({ pluginPaths: [], http: httpPreboot }); + + expect(initTranslationsMock).toHaveBeenCalledTimes(1); + expect(initTranslationsMock).toHaveBeenCalledWith('en', translationFiles); + }); + + it('calls `registerRoutesMock` with the correct parameters', async () => { + await service.preboot({ pluginPaths: [], http: httpPreboot }); + + expect(registerRoutesMock).toHaveBeenCalledTimes(1); + expect(registerRoutesMock).toHaveBeenCalledWith({ + locale: 'en', + router: expect.any(Object), + }); + }); }); describe('#setup', () => { + beforeEach(async () => { + await service.preboot({ pluginPaths: ['/pathPrebootA'], http: httpPreboot }); + + // Reset mocks that were used in the `preboot`. + getKibanaTranslationFilesMock.mockClear(); + initTranslationsMock.mockClear(); + registerRoutesMock.mockClear(); + }); + it('calls `getKibanaTranslationFiles` with the correct parameters', async () => { getKibanaTranslationFilesMock.mockResolvedValue([]); const pluginPaths = ['/pathA', '/pathB']; - await service.setup({ pluginPaths, http }); + await service.setup({ pluginPaths, http: httpSetup }); expect(getKibanaTranslationFilesMock).toHaveBeenCalledTimes(1); expect(getKibanaTranslationFilesMock).toHaveBeenCalledWith('en', pluginPaths); @@ -62,14 +108,14 @@ describe('I18nService', () => { const translationFiles = ['/path/to/file', 'path/to/another/file']; getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); - await service.setup({ pluginPaths: [], http }); + await service.setup({ pluginPaths: [], http: httpSetup }); expect(initTranslationsMock).toHaveBeenCalledTimes(1); expect(initTranslationsMock).toHaveBeenCalledWith('en', translationFiles); }); it('calls `registerRoutesMock` with the correct parameters', async () => { - await service.setup({ pluginPaths: [], http }); + await service.setup({ pluginPaths: [], http: httpSetup }); expect(registerRoutesMock).toHaveBeenCalledTimes(1); expect(registerRoutesMock).toHaveBeenCalledWith({ @@ -82,7 +128,10 @@ describe('I18nService', () => { const translationFiles = ['/path/to/file', 'path/to/another/file']; getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); - const { getLocale, getTranslationFiles } = await service.setup({ pluginPaths: [], http }); + const { getLocale, getTranslationFiles } = await service.setup({ + pluginPaths: [], + http: httpSetup, + }); expect(getLocale()).toEqual('en'); expect(getTranslationFiles()).toEqual(translationFiles); diff --git a/src/core/server/i18n/i18n_service.ts b/src/core/server/i18n/i18n_service.ts index 0dffd8934a8ca..02a809b3c2c36 100644 --- a/src/core/server/i18n/i18n_service.ts +++ b/src/core/server/i18n/i18n_service.ts @@ -10,12 +10,17 @@ import { take } from 'rxjs/operators'; import { Logger } from '../logging'; import { IConfigService } from '../config'; import { CoreContext } from '../core_context'; -import { InternalHttpServiceSetup } from '../http'; +import { InternalHttpServicePreboot, InternalHttpServiceSetup } from '../http'; import { config as i18nConfigDef, I18nConfigType } from './i18n_config'; import { getKibanaTranslationFiles } from './get_kibana_translation_files'; import { initTranslations } from './init_translations'; import { registerRoutes } from './routes'; +interface PrebootDeps { + http: InternalHttpServicePreboot; + pluginPaths: string[]; +} + interface SetupDeps { http: InternalHttpServiceSetup; pluginPaths: string[]; @@ -45,7 +50,24 @@ export class I18nService { this.configService = coreContext.configService; } + public async preboot({ pluginPaths, http }: PrebootDeps) { + const { locale } = await this.initTranslations(pluginPaths); + http.registerRoutes('', (router) => registerRoutes({ router, locale })); + } + public async setup({ pluginPaths, http }: SetupDeps): Promise { + const { locale, translationFiles } = await this.initTranslations(pluginPaths); + + const router = http.createRouter(''); + registerRoutes({ router, locale }); + + return { + getLocale: () => locale, + getTranslationFiles: () => translationFiles, + }; + } + + private async initTranslations(pluginPaths: string[]) { const i18nConfig = await this.configService .atPath(i18nConfigDef.path) .pipe(take(1)) @@ -59,12 +81,6 @@ export class I18nService { this.log.debug(`Using translation files: [${translationFiles.join(', ')}]`); await initTranslations(locale, translationFiles); - const router = http.createRouter(''); - registerRoutes({ router, locale }); - - return { - getLocale: () => locale, - getTranslationFiles: () => translationFiles, - }; + return { locale, translationFiles }; } } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index d2a4b4bff3390..c77a3a967364c 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -35,8 +35,9 @@ import { configSchema as elasticsearchConfigSchema, ElasticsearchServiceStart, IScopedClusterClient, + ElasticsearchServicePreboot, } from './elasticsearch'; -import { HttpServiceSetup, HttpServiceStart } from './http'; +import { HttpServicePreboot, HttpServiceSetup, HttpServiceStart } from './http'; import { HttpResources } from './http_resources'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; @@ -58,7 +59,7 @@ import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logg import { CoreUsageDataStart } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; import { DeprecationsServiceSetup } from './deprecations'; -// Because of #79265 we need to explicity import, then export these types for +// Because of #79265 we need to explicitly import, then export these types for // scripts/telemetry_check.js to work as expected import { CoreUsageStats, @@ -68,6 +69,9 @@ import { CoreEnvironmentUsageData, CoreServicesUsageData, } from './core_usage_data'; +import { PrebootServicePreboot } from './preboot'; + +export type { PrebootServicePreboot } from './preboot'; export type { CoreUsageStats, @@ -81,8 +85,6 @@ export type { import type { ExecutionContextSetup, ExecutionContextStart } from './execution_context'; export type { - ExecutionContextSetup, - ExecutionContextStart, IExecutionContextContainer, KibanaServerExecutionContext, KibanaExecutionContext, @@ -127,6 +129,7 @@ export type { LegacyElasticsearchClientConfig, LegacyElasticsearchError, LegacyElasticsearchErrorHelpers, + ElasticsearchServicePreboot, ElasticsearchServiceSetup, ElasticsearchServiceStart, ElasticsearchStatusMeta, @@ -145,6 +148,7 @@ export type { ShardsResponse, GetResponse, DeleteDocumentResponse, + ElasticsearchConfigPreboot, } from './elasticsearch'; export type { @@ -181,6 +185,7 @@ export type { HttpResponseOptions, HttpResponsePayload, HttpServerInfo, + HttpServicePreboot, HttpServiceSetup, HttpServiceStart, ErrorHttpResponseOptions, @@ -262,8 +267,11 @@ export type { AppenderConfigType, } from './logging'; +export { PluginType } from './plugins'; + export type { DiscoveredPlugin, + PrebootPlugin, Plugin, AsyncPlugin, PluginConfigDescriptor, @@ -470,7 +478,20 @@ export interface RequestHandlerContext { } /** - * Context passed to the plugins `setup` method. + * Context passed to the `setup` method of `preboot` plugins. + * @public + */ +export interface CorePreboot { + /** {@link ElasticsearchServicePreboot} */ + elasticsearch: ElasticsearchServicePreboot; + /** {@link HttpServicePreboot} */ + http: HttpServicePreboot; + /** {@link PrebootServicePreboot} */ + preboot: PrebootServicePreboot; +} + +/** + * Context passed to the `setup` method of `standard` plugins. * * @typeParam TPluginsStart - the type of the consuming plugin's start dependencies. Should be the same * as the consuming {@link Plugin}'s `TPluginsStart` type. Used by `getStartServices`. @@ -551,6 +572,8 @@ export type { CapabilitiesSetup, CapabilitiesStart, ContextSetup, + ExecutionContextSetup, + ExecutionContextStart, HttpResources, PluginsServiceSetup, PluginsServiceStart, diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index f3253e32aa08e..540670274f416 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -10,23 +10,32 @@ import { Type } from '@kbn/config-schema'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { ConfigDeprecationProvider } from './config'; -import { ContextSetup } from './context'; +import { InternalContextPreboot, ContextSetup } from './context'; import { + InternalElasticsearchServicePreboot, InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart, } from './elasticsearch'; -import { InternalHttpServiceSetup, InternalHttpServiceStart } from './http'; +import { + InternalHttpServicePreboot, + InternalHttpServiceSetup, + InternalHttpServiceStart, +} from './http'; import { InternalSavedObjectsServiceSetup, InternalSavedObjectsServiceStart, } from './saved_objects'; -import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; +import { + InternalUiSettingsServicePreboot, + InternalUiSettingsServiceSetup, + InternalUiSettingsServiceStart, +} from './ui_settings'; import { InternalEnvironmentServiceSetup } from './environment'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart } from './metrics'; import { InternalRenderingServiceSetup } from './rendering'; -import { InternalHttpResourcesSetup } from './http_resources'; +import { InternalHttpResourcesPreboot, InternalHttpResourcesSetup } from './http_resources'; import { InternalStatusServiceSetup } from './status'; -import { InternalLoggingServiceSetup } from './logging'; +import { InternalLoggingServicePreboot, InternalLoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; import { InternalDeprecationsServiceSetup } from './deprecations'; @@ -34,6 +43,18 @@ import type { InternalExecutionContextSetup, InternalExecutionContextStart, } from './execution_context'; +import { InternalPrebootServicePreboot } from './preboot'; + +/** @internal */ +export interface InternalCorePreboot { + context: InternalContextPreboot; + http: InternalHttpServicePreboot; + elasticsearch: InternalElasticsearchServicePreboot; + uiSettings: InternalUiSettingsServicePreboot; + httpResources: InternalHttpResourcesPreboot; + logging: InternalLoggingServicePreboot; + preboot: InternalPrebootServicePreboot; +} /** @internal */ export interface InternalCoreSetup { diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts index 88c45962ce4a6..d8f9f035f44be 100644 --- a/src/core/server/legacy/integration_tests/logging.test.ts +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -67,6 +67,7 @@ describe('logging service', () => { beforeAll(async () => { root = createRoot(); + await root.preboot(); await root.setup(); await root.start(); }, 30000); @@ -119,6 +120,7 @@ describe('logging service', () => { it('"silent": true', async () => { root = createRoot({ silent: true }); + await root.preboot(); await root.setup(); await root.start(); @@ -150,6 +152,7 @@ describe('logging service', () => { it('"quiet": true', async () => { root = createRoot({ quiet: true }); + await root.preboot(); await root.setup(); await root.start(); @@ -187,6 +190,7 @@ describe('logging service', () => { it('"verbose": true', async () => { root = createRoot({ verbose: true }); + await root.preboot(); await root.setup(); await root.start(); diff --git a/src/core/server/logging/index.ts b/src/core/server/logging/index.ts index 9d17b289bfa4c..ba6aec5f128c1 100644 --- a/src/core/server/logging/index.ts +++ b/src/core/server/logging/index.ts @@ -32,6 +32,10 @@ export type { export { LoggingSystem } from './logging_system'; export type { ILoggingSystem } from './logging_system'; export { LoggingService } from './logging_service'; -export type { InternalLoggingServiceSetup, LoggingServiceSetup } from './logging_service'; +export type { + InternalLoggingServicePreboot, + InternalLoggingServiceSetup, + LoggingServiceSetup, +} from './logging_service'; export { appendersSchema } from './appenders/appenders'; export type { AppenderConfigType } from './appenders/appenders'; diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index b4eb98546de21..ade10fc1c0257 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -49,6 +49,7 @@ describe('logging service', () => { mockConsoleLog = jest.spyOn(global.console, 'log'); root = createRoot(); + await root.preboot(); await root.setup(); }, 30000); @@ -151,6 +152,7 @@ describe('logging service', () => { mockConsoleLog = jest.spyOn(global.console, 'log'); root = kbnTestServer.createRoot(); + await root.preboot(); setup = await root.setup(); setup.logging.configure(['plugins', 'myplugin'], loggingConfig$); }, 30000); diff --git a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts index b40ce7a4e7b0e..b560748026ace 100644 --- a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts +++ b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts @@ -79,6 +79,7 @@ describe('RollingFileAppender', () => { pattern: '.%i', }, }); + await root.preboot(); await root.setup(); const logger = root.logger.get('test.rolling.file'); @@ -124,6 +125,7 @@ describe('RollingFileAppender', () => { pattern: '-%i', }, }); + await root.preboot(); await root.setup(); const logger = root.logger.get('test.rolling.file'); @@ -174,6 +176,7 @@ describe('RollingFileAppender', () => { pattern: '-%i', }, }); + await root.preboot(); await root.setup(); const logger = root.logger.get('test.rolling.file'); diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts index 5f91c7b8734b8..75d358c8cbe6e 100644 --- a/src/core/server/logging/logging_service.mock.ts +++ b/src/core/server/logging/logging_service.mock.ts @@ -12,8 +12,13 @@ import { LoggingService, LoggingServiceSetup, InternalLoggingServiceSetup, + InternalLoggingServicePreboot, } from './logging_service'; +const createInternalPrebootMock = (): jest.Mocked => ({ + configure: jest.fn(), +}); + const createInternalSetupMock = (): jest.Mocked => ({ configure: jest.fn(), }); @@ -25,11 +30,13 @@ const createSetupMock = (): jest.Mocked => ({ type LoggingServiceContract = PublicMethodsOf; const createMock = (): jest.Mocked => { const service: jest.Mocked = { + preboot: jest.fn(), setup: jest.fn(), start: jest.fn(), stop: jest.fn(), }; + service.preboot.mockReturnValue(createInternalPrebootMock()); service.setup.mockReturnValue(createInternalSetupMock()); return service; @@ -38,5 +45,6 @@ const createMock = (): jest.Mocked => { export const loggingServiceMock = { create: createMock, createSetupContract: createSetupMock, + createInternalPrebootContract: createInternalPrebootMock, createInternalSetupContract: createInternalSetupMock, }; diff --git a/src/core/server/logging/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts index 341a04736b87a..9817f08c59bf8 100644 --- a/src/core/server/logging/logging_service.test.ts +++ b/src/core/server/logging/logging_service.test.ts @@ -8,83 +8,102 @@ import { of, Subject } from 'rxjs'; -import { LoggingService, InternalLoggingServiceSetup } from './logging_service'; +import { + LoggingService, + InternalLoggingServiceSetup, + InternalLoggingServicePreboot, +} from './logging_service'; import { loggingSystemMock } from './logging_system.mock'; import { LoggerContextConfigType } from './logging_config'; describe('LoggingService', () => { let loggingSystem: ReturnType; let service: LoggingService; - let setup: InternalLoggingServiceSetup; + let preboot: InternalLoggingServicePreboot; beforeEach(() => { loggingSystem = loggingSystemMock.create(); service = new LoggingService({ logger: loggingSystem.asLoggerFactory() } as any); - setup = service.setup({ loggingSystem }); + preboot = service.preboot({ loggingSystem }); }); afterEach(() => { service.stop(); }); - describe('setup', () => { - it('forwards configuration changes to logging system', () => { - const config1: LoggerContextConfigType = { - appenders: new Map(), - loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], - }; - const config2: LoggerContextConfigType = { - appenders: new Map(), - loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }], - }; + function runTestSuite( + testSuiteName: string, + getContract: () => InternalLoggingServicePreboot | InternalLoggingServiceSetup + ) { + describe(testSuiteName, () => { + it('forwards configuration changes to logging system', async () => { + const config1: LoggerContextConfigType = { + appenders: new Map(), + loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], + }; + const config2: LoggerContextConfigType = { + appenders: new Map(), + loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }], + }; - setup.configure(['test', 'context'], of(config1, config2)); - expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith( - 1, - ['test', 'context'], - config1 - ); - expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith( - 2, - ['test', 'context'], - config2 - ); - }); + getContract().configure(['test', 'context'], of(config1, config2)); + expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith( + 1, + ['test', 'context'], + config1 + ); + expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith( + 2, + ['test', 'context'], + config2 + ); + }); - it('stops forwarding first observable when called a second time', () => { - const updates$ = new Subject(); - const config1: LoggerContextConfigType = { - appenders: new Map(), - loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], - }; - const config2: LoggerContextConfigType = { - appenders: new Map(), - loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }], - }; + it('stops forwarding first observable when called a second time', () => { + const updates$ = new Subject(); + const config1: LoggerContextConfigType = { + appenders: new Map(), + loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], + }; + const config2: LoggerContextConfigType = { + appenders: new Map(), + loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }], + }; - setup.configure(['test', 'context'], updates$); - setup.configure(['test', 'context'], of(config1)); - updates$.next(config2); - expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith( - 1, - ['test', 'context'], - config1 - ); - expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(['test', 'context'], config2); + const contract = getContract(); + contract.configure(['test', 'context'], updates$); + contract.configure(['test', 'context'], of(config1)); + updates$.next(config2); + expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith( + 1, + ['test', 'context'], + config1 + ); + expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith( + ['test', 'context'], + config2 + ); + }); }); - }); - describe('stop', () => { - it('stops forwarding updates to logging system', () => { - const updates$ = new Subject(); - const config1: LoggerContextConfigType = { - appenders: new Map(), - loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], - }; + describe(`stop after ${testSuiteName}`, () => { + it('stops forwarding updates to logging system', () => { + const updates$ = new Subject(); + const config1: LoggerContextConfigType = { + appenders: new Map(), + loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], + }; - setup.configure(['test', 'context'], updates$); - service.stop(); - updates$.next(config1); - expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(['test', 'context'], config1); + getContract().configure(['test', 'context'], updates$); + service.stop(); + updates$.next(config1); + expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith( + ['test', 'context'], + config1 + ); + }); }); - }); + } + + runTestSuite('preboot', () => preboot); + runTestSuite('setup', () => service.setup()); }); diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts index f5a4717fdbfaf..6c3eee4981725 100644 --- a/src/core/server/logging/logging_service.ts +++ b/src/core/server/logging/logging_service.ts @@ -42,11 +42,14 @@ export interface LoggingServiceSetup { } /** @internal */ -export interface InternalLoggingServiceSetup { +export interface InternalLoggingServicePreboot { configure(contextParts: string[], config$: Observable): void; } -interface SetupDeps { +/** @internal */ +export type InternalLoggingServiceSetup = InternalLoggingServicePreboot; + +interface PrebootDeps { loggingSystem: ILoggingSystem; } @@ -54,13 +57,14 @@ interface SetupDeps { export class LoggingService implements CoreService { private readonly subscriptions = new Map(); private readonly log: Logger; + private internalPreboot?: InternalLoggingServicePreboot; constructor(coreContext: CoreContext) { this.log = coreContext.logger.get('logging'); } - public setup({ loggingSystem }: SetupDeps) { - return { + public preboot({ loggingSystem }: PrebootDeps) { + this.internalPreboot = { configure: (contextParts: string[], config$: Observable) => { const contextName = LoggingConfig.getLoggerContext(contextParts); this.log.debug(`Setting custom config for context [${contextName}]`); @@ -80,6 +84,14 @@ export class LoggingService implements CoreService ); }, }; + + return this.internalPreboot; + } + + public setup() { + return { + configure: this.internalPreboot!.configure, + }; } public start() {} diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts index 19e3e6a6c68f9..93589648ca0ae 100644 --- a/src/core/server/metrics/integration_tests/server_collector.test.ts +++ b/src/core/server/metrics/integration_tests/server_collector.test.ts @@ -29,6 +29,7 @@ describe('ServerMetricsCollector', () => { beforeEach(async () => { server = createHttpServer(); + await server.preboot({ context: contextServiceMock.createPrebootContract() }); const contextSetup = contextServiceMock.createSetupContract(); const httpSetup = await server.setup({ context: contextSetup, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index ff844f44aede0..f423e40fbcaf9 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -10,7 +10,13 @@ import { of } from 'rxjs'; import { duration } from 'moment'; import { ByteSizeValue } from '@kbn/config-schema'; import type { MockedKeys } from '@kbn/utility-types/jest'; -import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + StartServicesAccessor, + CorePreboot, +} from '.'; import { loggingSystemMock } from './logging/logging_system.mock'; import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; @@ -31,6 +37,7 @@ import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_serv import { i18nServiceMock } from './i18n/i18n_service.mock'; import { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; import { executionContextServiceMock } from './execution_context/execution_context_service.mock'; +import { prebootServiceMock } from './preboot/preboot_service.mock'; export { configServiceMock } from './config/mocks'; export { httpServerMock } from './http/http_server.mocks'; @@ -106,6 +113,7 @@ function pluginInitializerContextMock(config: T = {} as T) { dist: false, }, instanceUuid: 'instance-uuid', + configs: ['/some/path/to/config/kibana.yml'], }, config: pluginInitializerContextConfigMock(config), }; @@ -113,6 +121,20 @@ function pluginInitializerContextMock(config: T = {} as T) { return mock; } +type CorePrebootMockType = MockedKeys & { + elasticsearch: ReturnType; +}; + +function createCorePrebootMock() { + const mock: CorePrebootMockType = { + elasticsearch: elasticsearchServiceMock.createPreboot(), + http: httpServiceMock.createPrebootContract(), + preboot: prebootServiceMock.createPrebootContract(), + }; + + return mock; +} + type CoreSetupMockType = MockedKeys & { elasticsearch: ReturnType; getStartServices: jest.MockedFunction>; @@ -170,6 +192,19 @@ function createCoreStartMock() { return mock; } +function createInternalCorePrebootMock() { + const prebootDeps = { + context: contextServiceMock.createPrebootContract(), + elasticsearch: elasticsearchServiceMock.createInternalPreboot(), + http: httpServiceMock.createInternalPrebootContract(), + httpResources: httpResourcesMock.createPrebootContract(), + uiSettings: uiSettingsServiceMock.createPrebootContract(), + logging: loggingServiceMock.createInternalPrebootContract(), + preboot: prebootServiceMock.createInternalPrebootContract(), + }; + return prebootDeps; +} + function createInternalCoreSetupMock() { const setupDeps = { capabilities: capabilitiesServiceMock.createSetupContract(), @@ -227,8 +262,10 @@ function createCoreRequestHandlerContextMock() { } export const coreMock = { + createPreboot: createCorePrebootMock, createSetup: createCoreSetupMock, createStart: createCoreStartMock, + createInternalPreboot: createInternalCorePrebootMock, createInternalSetup: createInternalCoreSetupMock, createInternalStart: createInternalCoreStartMock, createPluginInitializerContext: pluginInitializerContextMock, diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts index f3a92c896b014..3e410e4b19c0e 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts @@ -226,6 +226,29 @@ test('return error when manifest contains unrecognized properties', async () => }); }); +test('returns error when manifest contains unrecognized `type`', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb( + null, + Buffer.from( + JSON.stringify({ + id: 'someId', + version: '7.0.0', + kibanaVersion: '7.0.0', + type: 'unknown', + server: true, + }) + ) + ); + }); + + await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + message: `The "type" in manifest for plugin "someId" is set to "unknown", but it should either be "standard" or "preboot". (invalid-manifest, ${pluginManifestPath})`, + type: PluginDiscoveryErrorType.InvalidManifest, + path: pluginManifestPath, + }); +}); + describe('configPath', () => { test('falls back to plugin id if not specified', async () => { mockReadFile.mockImplementation((path, cb) => { @@ -284,6 +307,7 @@ test('set defaults for all missing optional fields', async () => { configPath: 'some_id', version: '7.0.0', kibanaVersion: '7.0.0', + type: 'standard', optionalPlugins: [], requiredPlugins: [], requiredBundles: [], @@ -302,6 +326,7 @@ test('return all set optional fields as they are in manifest', async () => { configPath: ['some', 'path'], version: 'some-version', kibanaVersion: '7.0.0', + type: 'preboot', requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], optionalPlugins: ['some-optional-plugin'], ui: true, @@ -315,6 +340,7 @@ test('return all set optional fields as they are in manifest', async () => { configPath: ['some', 'path'], version: 'some-version', kibanaVersion: '7.0.0', + type: 'preboot', optionalPlugins: ['some-optional-plugin'], requiredBundles: [], requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], @@ -345,6 +371,7 @@ test('return manifest when plugin expected Kibana version matches actual version configPath: 'some-path', version: 'some-version', kibanaVersion: '7.0.0-alpha2', + type: 'standard', optionalPlugins: [], requiredPlugins: ['some-required-plugin'], requiredBundles: [], @@ -375,6 +402,7 @@ test('return manifest when plugin expected Kibana version is `kibana`', async () configPath: 'some_id', version: 'some-version', kibanaVersion: 'kibana', + type: 'standard', optionalPlugins: [], requiredPlugins: ['some-required-plugin'], requiredBundles: [], diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index b59418a67219e..57640ec6acc0a 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -12,7 +12,7 @@ import { coerce } from 'semver'; import { promisify } from 'util'; import { snakeCase } from 'lodash'; import { isConfigPath, PackageInfo } from '../../config'; -import { PluginManifest } from '../types'; +import { PluginManifest, PluginType } from '../types'; import { PluginDiscoveryError } from './plugin_discovery_error'; import { isCamelCase } from './is_camel_case'; @@ -39,6 +39,7 @@ const KNOWN_MANIFEST_FIELDS = (() => { const manifestFields: { [P in keyof PluginManifest]: boolean } = { id: true, kibanaVersion: true, + type: true, version: true, configPath: true, requiredPlugins: true, @@ -178,10 +179,21 @@ export async function parseManifest( ); } + const type = manifest.type ?? PluginType.standard; + if (type !== PluginType.preboot && type !== PluginType.standard) { + throw PluginDiscoveryError.invalidManifest( + manifestPath, + new Error( + `The "type" in manifest for plugin "${manifest.id}" is set to "${type}", but it should either be "standard" or "preboot".` + ) + ); + } + return { id: manifest.id, version: manifest.version, kibanaVersion: expectedKibanaVersion, + type, configPath: manifest.configPath || snakeCase(manifest.id), requiredPlugins: Array.isArray(manifest.requiredPlugins) ? manifest.requiredPlugins : [], optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [], diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index f6028fa8b099d..28f2ab799e092 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -21,6 +21,7 @@ import { PluginsConfig, PluginsConfigType, config } from '../plugins_config'; import type { InstanceInfo } from '../plugin_context'; import { discover } from './plugins_discovery'; import { CoreContext } from '../../core_context'; +import { PluginType } from '../types'; const KIBANA_ROOT = process.cwd(); @@ -34,6 +35,15 @@ const Plugins = { incompatible: () => ({ 'kibana.json': JSON.stringify({ id: 'plugin', version: '1' }), }), + incompatibleType: (id: string) => ({ + 'kibana.json': JSON.stringify({ + id, + version: '1', + kibanaVersion: '1.2.3', + type: 'evenEarlierThanPreboot', + server: true, + }), + }), missingManifest: () => ({}), inaccessibleManifest: () => ({ 'kibana.json': mockFs.file({ @@ -52,6 +62,18 @@ const Plugins = { server: true, }), }), + validPreboot: (id: string) => ({ + 'kibana.json': JSON.stringify({ + id, + configPath: ['plugins', id], + version: '1', + kibanaVersion: '1.2.3', + type: PluginType.preboot, + requiredPlugins: [], + optionalPlugins: [], + server: true, + }), + }), }; const packageMock = { @@ -132,6 +154,7 @@ describe('plugins discovery system', () => { [`${KIBANA_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), [`${KIBANA_ROOT}/plugins/plugin_b`]: Plugins.valid('pluginB'), [`${KIBANA_ROOT}/x-pack/plugins/plugin_c`]: Plugins.valid('pluginC'), + [`${KIBANA_ROOT}/src/plugins/plugin_d`]: Plugins.validPreboot('pluginD'), }, { createCwd: false } ); @@ -139,8 +162,10 @@ describe('plugins discovery system', () => { const plugins = await plugin$.pipe(toArray()).toPromise(); const pluginNames = plugins.map((plugin) => plugin.name); - expect(pluginNames).toHaveLength(3); - expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB', 'pluginC'])); + expect(pluginNames).toHaveLength(4); + expect(pluginNames).toEqual( + expect.arrayContaining(['pluginA', 'pluginB', 'pluginC', 'pluginD']) + ); }); it('return errors when the manifest is invalid or incompatible', async () => { @@ -155,6 +180,7 @@ describe('plugins discovery system', () => { [`${KIBANA_ROOT}/src/plugins/plugin_a`]: Plugins.invalid(), [`${KIBANA_ROOT}/src/plugins/plugin_b`]: Plugins.incomplete(), [`${KIBANA_ROOT}/src/plugins/plugin_c`]: Plugins.incompatible(), + [`${KIBANA_ROOT}/src/plugins/plugin_d`]: Plugins.incompatibleType('pluginD'), [`${KIBANA_ROOT}/src/plugins/plugin_ad`]: Plugins.missingManifest(), }, { createCwd: false } @@ -181,6 +207,9 @@ describe('plugins discovery system', () => { `Error: Plugin "plugin" is only compatible with Kibana version "1", but used Kibana version is "1.2.3". (incompatible-version, ${manifestPath( 'plugin_c' )})`, + `Error: The "type" in manifest for plugin "pluginD" is set to "evenEarlierThanPreboot", but it should either be "standard" or "preboot". (invalid-manifest, ${manifestPath( + 'plugin_d' + )})`, ]) ); }); @@ -271,7 +300,8 @@ describe('plugins discovery system', () => { [`${KIBANA_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), [`${KIBANA_ROOT}/src/plugins/sub1/plugin_b`]: Plugins.valid('pluginB'), [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_c`]: Plugins.valid('pluginC'), - [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_d`]: Plugins.incomplete(), + [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_d`]: Plugins.validPreboot('pluginD'), + [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_e`]: Plugins.incomplete(), }, { createCwd: false } ); @@ -279,8 +309,10 @@ describe('plugins discovery system', () => { const plugins = await plugin$.pipe(toArray()).toPromise(); const pluginNames = plugins.map((plugin) => plugin.name); - expect(pluginNames).toHaveLength(3); - expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB', 'pluginC'])); + expect(pluginNames).toHaveLength(4); + expect(pluginNames).toEqual( + expect.arrayContaining(['pluginA', 'pluginB', 'pluginC', 'pluginD']) + ); const errors = await error$ .pipe( @@ -294,7 +326,7 @@ describe('plugins discovery system', () => { `Error: Plugin manifest must contain an "id" property. (invalid-manifest, ${manifestPath( 'sub1', 'sub2', - 'plugin_d' + 'plugin_e' )})`, ]) ); @@ -358,6 +390,7 @@ describe('plugins discovery system', () => { [pluginFolder]: { plugin_a: Plugins.valid('pluginA'), plugin_b: Plugins.valid('pluginB'), + plugin_c: Plugins.validPreboot('pluginC'), }, }, { createCwd: false } @@ -366,8 +399,8 @@ describe('plugins discovery system', () => { const plugins = await plugin$.pipe(toArray()).toPromise(); const pluginNames = plugins.map((plugin) => plugin.name); - expect(pluginNames).toHaveLength(2); - expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB'])); + expect(pluginNames).toHaveLength(3); + expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB', 'pluginC'])); }); it('logs a warning about --plugin-path when used in development', async () => { diff --git a/src/core/server/plugins/index.ts b/src/core/server/plugins/index.ts index a71df00b39c5c..1b655ccd8bd98 100644 --- a/src/core/server/plugins/index.ts +++ b/src/core/server/plugins/index.ts @@ -7,7 +7,12 @@ */ export { PluginsService } from './plugins_service'; -export type { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from './plugins_service'; +export type { + PluginsServiceSetup, + PluginsServiceStart, + UiPlugins, + DiscoveredPlugins, +} from './plugins_service'; export { config } from './plugins_config'; /** @internal */ export { isNewPlatformPlugin } from './discovery'; diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index a29fb01fbc009..1b0caf7bf6255 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -20,12 +20,12 @@ import { config } from '../plugins_config'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { environmentServiceMock } from '../../environment/environment_service.mock'; import { coreMock } from '../../mocks'; -import { AsyncPlugin } from '../types'; +import { AsyncPlugin, PluginType } from '../types'; import { PluginWrapper } from '../plugin'; describe('PluginsService', () => { const logger = loggingSystemMock.create(); - const environmentSetup = environmentServiceMock.createSetupContract(); + const environmentPreboot = environmentServiceMock.createPrebootContract(); let pluginsService: PluginsService; const createPlugin = ( @@ -38,6 +38,7 @@ describe('PluginsService', () => { requiredBundles = [], optionalPlugins = [], kibanaVersion = '7.0.0', + type = PluginType.standard, configPath = [path], server = true, ui = true, @@ -49,6 +50,7 @@ describe('PluginsService', () => { requiredBundles?: string[]; optionalPlugins?: string[]; kibanaVersion?: string; + type?: PluginType; configPath?: ConfigPath; server?: boolean; ui?: boolean; @@ -61,6 +63,7 @@ describe('PluginsService', () => { version, configPath: `${configPath}${disabled ? '-disabled' : ''}`, kibanaVersion, + type, requiredPlugins, requiredBundles, optionalPlugins, @@ -150,7 +153,10 @@ describe('PluginsService', () => { } ); - await pluginsService.discover({ environment: environmentSetup }); + await pluginsService.discover({ environment: environmentPreboot }); + + const prebootDeps = coreMock.createInternalPreboot(); + await pluginsService.preboot(prebootDeps); const setupDeps = coreMock.createInternalSetup(); await pluginsService.setup(setupDeps); diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index c90d2e804225c..610bc1cac5a3b 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -15,10 +15,10 @@ import { Env } from '../config'; import { CoreContext } from '../core_context'; import { coreMock } from '../mocks'; import { loggingSystemMock } from '../logging/logging_system.mock'; -import { getEnvOptions, configServiceMock } from '../config/mocks'; +import { configServiceMock, getEnvOptions } from '../config/mocks'; import { PluginWrapper } from './plugin'; -import { PluginManifest } from './types'; +import { PluginManifest, PluginType } from './types'; import { createPluginInitializerContext, createPluginSetupContext, @@ -45,6 +45,7 @@ function createPluginManifest(manifestProps: Partial = {}): Plug version: 'some-version', configPath: 'path', kibanaVersion: '7.0.0', + type: PluginType.standard, requiredPlugins: ['some-required-dep'], optionalPlugins: ['some-optional-dep'], requiredBundles: [], @@ -243,6 +244,31 @@ test('`start` fails if setup is not called first', () => { ); }); +test('`start` fails invoked for the `preboot` plugin', async () => { + const manifest = createPluginManifest({ type: PluginType.preboot }); + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', + manifest, + opaqueId, + initializerContext: createPluginInitializerContext( + coreContext, + opaqueId, + manifest, + instanceInfo + ), + }); + + const mockPluginInstance = { setup: jest.fn() }; + mockPluginInitializer.mockReturnValue(mockPluginInstance); + + await plugin.setup({} as any, {} as any); + + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( + `"Plugin \\"some-plugin-id\\" is a preboot plugin and cannot be started."` + ); +}); + test('`start` calls plugin.start with context and dependencies', async () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index ca7f11e28de75..76ed4f8f24b3d 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -15,15 +15,17 @@ import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../logging'; import { - Plugin, AsyncPlugin, + Plugin, + PluginConfigDescriptor, + PluginInitializer, PluginInitializerContext, PluginManifest, - PluginInitializer, PluginOpaqueId, - PluginConfigDescriptor, + PluginType, + PrebootPlugin, } from './types'; -import { CoreSetup, CoreStart } from '..'; +import { CorePreboot, CoreSetup, CoreStart } from '..'; /** * Lightweight wrapper around discovered plugin that is responsible for instantiating @@ -53,6 +55,7 @@ export class PluginWrapper< private instance?: | Plugin + | PrebootPlugin | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); @@ -88,11 +91,16 @@ export class PluginWrapper< * is the contract returned by the dependency's `setup` function. */ public setup( - setupContext: CoreSetup, + setupContext: CoreSetup | CorePreboot, plugins: TPluginsSetup ): TSetup | Promise { this.instance = this.createPluginInstance(); - return this.instance.setup(setupContext, plugins); + + if (this.isPrebootPluginInstance(this.instance)) { + return this.instance.setup(setupContext as CorePreboot, plugins); + } + + return this.instance.setup(setupContext as CoreSetup, plugins); } /** @@ -107,6 +115,10 @@ export class PluginWrapper< throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } + if (this.isPrebootPluginInstance(this.instance)) { + throw new Error(`Plugin "${this.name}" is a preboot plugin and cannot be started.`); + } + const startContract = this.instance.start(startContext, plugins); if (isPromise(startContract)) { return startContract.then((resolvedContract) => { @@ -185,4 +197,10 @@ export class PluginWrapper< return instance; } + + private isPrebootPluginInstance( + instance: PluginWrapper['instance'] + ): instance is PrebootPlugin { + return this.manifest.type === PluginType.preboot; + } } diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 4ba34ccb2149f..7913bad3cad17 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -10,15 +10,21 @@ import { duration } from 'moment'; import { first } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/dev-utils'; import { fromRoot } from '@kbn/utils'; -import { createPluginInitializerContext, InstanceInfo } from './plugin_context'; +import { + createPluginInitializerContext, + createPluginPrebootSetupContext, + InstanceInfo, +} from './plugin_context'; import { CoreContext } from '../core_context'; import { Env } from '../config'; import { loggingSystemMock } from '../logging/logging_system.mock'; -import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; -import { PluginManifest } from './types'; +import { rawConfigServiceMock, getEnvOptions, configServiceMock } from '../config/mocks'; +import { PluginManifest, PluginType } from './types'; import { Server } from '../server'; import { schema, ByteSizeValue } from '@kbn/config-schema'; import { ConfigService } from '@kbn/config'; +import { PluginWrapper } from './plugin'; +import { coreMock } from '../mocks'; function createPluginManifest(manifestProps: Partial = {}): PluginManifest { return { @@ -26,6 +32,7 @@ function createPluginManifest(manifestProps: Partial = {}): Plug version: 'some-version', configPath: 'path', kibanaVersion: '7.0.0', + type: PluginType.standard, requiredPlugins: ['some-required-dep'], requiredBundles: [], optionalPlugins: ['some-optional-dep'], @@ -141,5 +148,67 @@ describe('createPluginInitializerContext', () => { ); expect(pluginInitializerContext.env.instanceUuid).toBe('kibana-uuid'); }); + + it('should expose paths to the config files', () => { + coreContext = { + ...coreContext, + env: Env.createDefault( + REPO_ROOT, + getEnvOptions({ + configs: ['/home/kibana/config/kibana.yml', '/home/kibana/config/kibana.dev.yml'], + }) + ), + }; + const pluginInitializerContext = createPluginInitializerContext( + coreContext, + opaqueId, + createPluginManifest(), + instanceInfo + ); + expect(pluginInitializerContext.env.configs).toEqual([ + '/home/kibana/config/kibana.yml', + '/home/kibana/config/kibana.dev.yml', + ]); + }); + }); +}); + +describe('createPluginPrebootSetupContext', () => { + let coreContext: CoreContext; + let opaqueId: symbol; + + beforeEach(async () => { + opaqueId = Symbol(); + coreContext = { + coreId: Symbol('core'), + env: Env.createDefault(REPO_ROOT, getEnvOptions()), + logger: loggingSystemMock.create(), + configService: configServiceMock.create(), + }; + }); + + it('`holdSetupUntilResolved` captures plugin.name', () => { + const manifest = createPluginManifest(); + const plugin = new PluginWrapper({ + path: 'some-path', + manifest, + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest, { + uuid: 'instance-uuid', + }), + }); + + const corePreboot = coreMock.createInternalPreboot(); + const prebootSetupContext = createPluginPrebootSetupContext(coreContext, corePreboot, plugin); + + const holdSetupPromise = Promise.resolve(undefined); + prebootSetupContext.preboot.holdSetupUntilResolved('some-reason', holdSetupPromise); + + expect(corePreboot.preboot.holdSetupUntilResolved).toHaveBeenCalledTimes(1); + expect(corePreboot.preboot.holdSetupUntilResolved).toHaveBeenCalledWith( + 'some-plugin-id', + 'some-reason', + holdSetupPromise + ); }); }); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 70fd1c60efa61..29194b1e8fc62 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -10,11 +10,15 @@ import { shareReplay } from 'rxjs/operators'; import type { RequestHandlerContext } from 'src/core/server'; import { CoreContext } from '../core_context'; import { PluginWrapper } from './plugin'; -import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; +import { + PluginsServicePrebootSetupDeps, + PluginsServiceSetupDeps, + PluginsServiceStartDeps, +} from './plugins_service'; import { PluginInitializerContext, PluginManifest, PluginOpaqueId } from './types'; import { IRouter, RequestHandlerContextProvider } from '../http'; import { getGlobalConfig, getGlobalConfig$ } from './legacy_config'; -import { CoreSetup, CoreStart } from '..'; +import { CorePreboot, CoreSetup, CoreStart } from '..'; export interface InstanceInfo { uuid: string; @@ -49,6 +53,7 @@ export function createPluginInitializerContext( mode: coreContext.env.mode, packageInfo: coreContext.env.packageInfo, instanceUuid: instanceInfo.uuid, + configs: coreContext.env.configs, }, /** @@ -83,6 +88,42 @@ export function createPluginInitializerContext( }; } +/** + * Provides `CorePreboot` contract that will be exposed to the `preboot` plugin `setup` method. + * This contract should be safe to use only within `setup` itself. + * + * This is called for each `preboot` plugin when it's set up, so each plugin gets its own + * version of these values. + * + * We should aim to be restrictive and specific in the APIs that we expose. + * + * @param coreContext Kibana core context + * @param deps Dependencies that Plugins services gets during setup. + * @param plugin The plugin we're building these values for. + * @internal + */ +export function createPluginPrebootSetupContext( + coreContext: CoreContext, + deps: PluginsServicePrebootSetupDeps, + plugin: PluginWrapper +): CorePreboot { + return { + elasticsearch: { + config: deps.elasticsearch.config, + createClient: deps.elasticsearch.createClient, + }, + http: { + registerRoutes: deps.http.registerRoutes, + basePath: deps.http.basePath, + }, + preboot: { + isSetupOnHold: deps.preboot.isSetupOnHold, + holdSetupUntilResolved: (reason, promise) => + deps.preboot.holdSetupUntilResolved(plugin.name, reason, promise), + }, + }; +} + /** * This returns a facade for `CoreContext` that will be exposed to the plugin `setup` method. * This facade should be safe to use only within `setup` itself. diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index f4f2263a1bdb0..ee7b35a412e80 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -20,6 +20,7 @@ const createStartContractMock = () => ({ contracts: new Map() }); const createServiceMock = (): PluginsServiceMock => ({ discover: jest.fn(), getExposedPluginConfigsToUsage: jest.fn(), + preboot: jest.fn(), setup: jest.fn().mockResolvedValue(createSetupContractMock()), start: jest.fn().mockResolvedValue(createStartContractMock()), stop: jest.fn(), diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 5c50df07dc697..3cd645cb74ac0 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -24,26 +24,31 @@ import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; import { take } from 'rxjs/operators'; -import { DiscoveredPlugin } from './types'; +import { DiscoveredPlugin, PluginType } from './types'; -const MockPluginsSystem: jest.Mock = PluginsSystem as any; +const MockPluginsSystem: jest.Mock> = PluginsSystem as any; let pluginsService: PluginsService; let config$: BehaviorSubject>; let configService: ConfigService; let coreId: symbol; let env: Env; -let mockPluginSystem: jest.Mocked; -let environmentSetup: ReturnType; +let prebootMockPluginSystem: jest.Mocked>; +let standardMockPluginSystem: jest.Mocked>; +let environmentPreboot: ReturnType; +const prebootDeps = coreMock.createInternalPreboot(); const setupDeps = coreMock.createInternalSetup(); +const startDeps = coreMock.createInternalStart(); const logger = loggingSystemMock.create(); expect.addSnapshotSerializer(createAbsolutePathSerializer()); ['path-1', 'path-2', 'path-3', 'path-4', 'path-5', 'path-6', 'path-7', 'path-8'].forEach((path) => { - jest.doMock(join(path, 'server'), () => ({}), { - virtual: true, + [PluginType.preboot, PluginType.standard].forEach((type) => { + jest.doMock(join(`${path}-${type}`, 'server'), () => ({}), { + virtual: true, + }); }); }); @@ -53,6 +58,7 @@ const createPlugin = ( path = id, disabled = false, version = 'some-version', + type = PluginType.standard, requiredPlugins = [], requiredBundles = [], optionalPlugins = [], @@ -64,6 +70,7 @@ const createPlugin = ( path?: string; disabled?: boolean; version?: string; + type?: PluginType; requiredPlugins?: string[]; requiredBundles?: string[]; optionalPlugins?: string[]; @@ -80,6 +87,7 @@ const createPlugin = ( version, configPath: disabled ? configPath.concat('-disabled') : configPath, kibanaVersion, + type, requiredPlugins, requiredBundles, optionalPlugins, @@ -111,11 +119,13 @@ async function testSetup() { await configService.setSchema(config.path, config.schema); pluginsService = new PluginsService({ coreId, env, logger, configService }); - [mockPluginSystem] = MockPluginsSystem.mock.instances as any; - mockPluginSystem.uiPlugins.mockReturnValue(new Map()); - mockPluginSystem.getPlugins.mockReturnValue([]); + [prebootMockPluginSystem, standardMockPluginSystem] = MockPluginsSystem.mock.instances as any; + prebootMockPluginSystem.uiPlugins.mockReturnValue(new Map()); + prebootMockPluginSystem.getPlugins.mockReturnValue([]); + standardMockPluginSystem.uiPlugins.mockReturnValue(new Map()); + standardMockPluginSystem.getPlugins.mockReturnValue([]); - environmentSetup = environmentServiceMock.createSetupContract(); + environmentPreboot = environmentServiceMock.createPrebootContract(); } afterEach(() => { @@ -134,7 +144,7 @@ describe('PluginsService', () => { plugin$: from([]), }); - await expect(pluginsService.discover({ environment: environmentSetup })).rejects + await expect(pluginsService.discover({ environment: environmentPreboot })).rejects .toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Invalid JSON (invalid-manifest, path-1)] @@ -156,7 +166,7 @@ describe('PluginsService', () => { plugin$: from([]), }); - await expect(pluginsService.discover({ environment: environmentSetup })).rejects + await expect(pluginsService.discover({ environment: environmentPreboot })).rejects .toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Incompatible version (incompatible-version, path-3)] @@ -175,14 +185,14 @@ describe('PluginsService', () => { error$: from([]), plugin$: from([ createPlugin('conflicting-id', { - path: 'path-4', + path: 'path-4-standard', version: 'some-version', configPath: 'path', requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], optionalPlugins: ['some-optional-plugin'], }), createPlugin('conflicting-id', { - path: 'path-4', + path: 'path-4-standard', version: 'some-version', configPath: 'path', requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], @@ -192,13 +202,49 @@ describe('PluginsService', () => { }); await expect( - pluginsService.discover({ environment: environmentSetup }) + pluginsService.discover({ environment: environmentPreboot }) ).rejects.toMatchInlineSnapshot( `[Error: Plugin with id "conflicting-id" is already registered!]` ); - expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); - expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + expect(prebootMockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + expect(standardMockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + }); + + it('throws if discovered standard and preboot plugins with conflicting names', async () => { + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('conflicting-id', { + type: PluginType.preboot, + path: 'path-4-preboot', + version: 'some-version', + configPath: 'path', + requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], + optionalPlugins: ['some-optional-plugin'], + }), + createPlugin('conflicting-id', { + path: 'path-4-standard', + version: 'some-version', + configPath: 'path', + requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], + optionalPlugins: ['some-optional-plugin'], + }), + ]), + }); + + await expect( + pluginsService.discover({ environment: environmentPreboot }) + ).rejects.toMatchInlineSnapshot( + `[Error: Plugin with id "conflicting-id" is already registered!]` + ); + + expect(prebootMockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + expect(standardMockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); }); it('properly detects plugins that should be disabled.', async () => { @@ -206,160 +252,356 @@ describe('PluginsService', () => { .spyOn(configService, 'isEnabledAtPath') .mockImplementation((path) => Promise.resolve(!path.includes('disabled'))); - mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); + prebootMockPluginSystem.setupPlugins.mockResolvedValue(new Map()); + standardMockPluginSystem.setupPlugins.mockResolvedValue(new Map()); mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - createPlugin('explicitly-disabled-plugin', { + createPlugin('explicitly-disabled-plugin-preboot', { + type: PluginType.preboot, + disabled: true, + path: 'path-1-preboot', + configPath: 'path-1-preboot', + }), + createPlugin('explicitly-disabled-plugin-standard', { disabled: true, - path: 'path-1', - configPath: 'path-1', + path: 'path-1-standard', + configPath: 'path-1-standard', + }), + createPlugin('plugin-with-missing-required-deps-preboot', { + type: PluginType.preboot, + path: 'path-2-preboot', + configPath: 'path-2-preboot', + requiredPlugins: ['missing-plugin-preboot'], }), - createPlugin('plugin-with-missing-required-deps', { - path: 'path-2', - configPath: 'path-2', - requiredPlugins: ['missing-plugin'], + createPlugin('plugin-with-missing-required-deps-standard', { + path: 'path-2-standard', + configPath: 'path-2-standard', + requiredPlugins: ['missing-plugin-standard'], }), - createPlugin('plugin-with-disabled-transitive-dep', { - path: 'path-3', - configPath: 'path-3', - requiredPlugins: ['another-explicitly-disabled-plugin'], + createPlugin('plugin-with-disabled-transitive-dep-preboot', { + type: PluginType.preboot, + path: 'path-3-preboot', + configPath: 'path-3-preboot', + requiredPlugins: ['another-explicitly-disabled-plugin-preboot'], }), - createPlugin('another-explicitly-disabled-plugin', { + createPlugin('plugin-with-disabled-transitive-dep-standard', { + path: 'path-3-standard', + configPath: 'path-3-standard', + requiredPlugins: ['another-explicitly-disabled-plugin-standard'], + }), + createPlugin('another-explicitly-disabled-plugin-preboot', { + type: PluginType.preboot, + disabled: true, + path: 'path-4-preboot', + configPath: 'path-4-disabled-preboot', + }), + createPlugin('another-explicitly-disabled-plugin-standard', { disabled: true, - path: 'path-4', - configPath: 'path-4-disabled', + path: 'path-4-standard', + configPath: 'path-4-disabled-standard', + }), + createPlugin('plugin-with-disabled-optional-dep-preboot', { + type: PluginType.preboot, + path: 'path-5-preboot', + configPath: 'path-5-preboot', + optionalPlugins: ['explicitly-disabled-plugin-preboot'], }), - createPlugin('plugin-with-disabled-optional-dep', { - path: 'path-5', - configPath: 'path-5', - optionalPlugins: ['explicitly-disabled-plugin'], + createPlugin('plugin-with-disabled-optional-dep-standard', { + path: 'path-5-standard', + configPath: 'path-5-standard', + optionalPlugins: ['explicitly-disabled-plugin-standard'], }), - createPlugin('plugin-with-missing-optional-dep', { - path: 'path-6', - configPath: 'path-6', - optionalPlugins: ['missing-plugin'], + createPlugin('plugin-with-missing-optional-dep-preboot', { + type: PluginType.preboot, + path: 'path-6-preboot', + configPath: 'path-6-preboot', + optionalPlugins: ['missing-plugin-preboot'], }), - createPlugin('plugin-with-disabled-nested-transitive-dep', { - path: 'path-7', - configPath: 'path-7', - requiredPlugins: ['plugin-with-disabled-transitive-dep'], + createPlugin('plugin-with-missing-optional-dep-standard', { + path: 'path-6-standard', + configPath: 'path-6-standard', + optionalPlugins: ['missing-plugin-standard'], }), - createPlugin('plugin-with-missing-nested-dep', { - path: 'path-8', - configPath: 'path-8', - requiredPlugins: ['plugin-with-missing-required-deps'], + createPlugin('plugin-with-disabled-nested-transitive-dep-preboot', { + type: PluginType.preboot, + path: 'path-7-preboot', + configPath: 'path-7-preboot', + requiredPlugins: ['plugin-with-disabled-transitive-dep-preboot'], + }), + createPlugin('plugin-with-disabled-nested-transitive-dep-standard', { + path: 'path-7-standard', + configPath: 'path-7-standard', + requiredPlugins: ['plugin-with-disabled-transitive-dep-standard'], + }), + createPlugin('plugin-with-missing-nested-dep-preboot', { + type: PluginType.preboot, + path: 'path-8-preboot', + configPath: 'path-8-preboot', + requiredPlugins: ['plugin-with-missing-required-deps-preboot'], + }), + createPlugin('plugin-with-missing-nested-dep-standard', { + path: 'path-8-standard', + configPath: 'path-8-standard', + requiredPlugins: ['plugin-with-missing-required-deps-standard'], }), ]), }); - await pluginsService.discover({ environment: environmentSetup }); + await pluginsService.discover({ environment: environmentPreboot }); + await pluginsService.preboot(prebootDeps); const setup = await pluginsService.setup(setupDeps); expect(setup.contracts).toBeInstanceOf(Map); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); - expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); - expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); + + expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); + expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); + + expect(prebootMockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); + expect(standardMockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); + + expect(prebootMockPluginSystem.setupPlugins).toHaveBeenCalledWith(prebootDeps); + expect(standardMockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); expect(loggingSystemMock.collect(logger).info).toMatchInlineSnapshot(` Array [ Array [ - "Plugin \\"explicitly-disabled-plugin\\" is disabled.", + "Plugin \\"explicitly-disabled-plugin-preboot\\" is disabled.", + ], + Array [ + "Plugin \\"explicitly-disabled-plugin-standard\\" is disabled.", + ], + Array [ + "Plugin \\"plugin-with-missing-required-deps-preboot\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [missing-plugin-preboot]", + ], + Array [ + "Plugin \\"plugin-with-missing-required-deps-standard\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [missing-plugin-standard]", + ], + Array [ + "Plugin \\"plugin-with-disabled-transitive-dep-preboot\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [another-explicitly-disabled-plugin-preboot]", + ], + Array [ + "Plugin \\"plugin-with-disabled-transitive-dep-standard\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [another-explicitly-disabled-plugin-standard]", ], Array [ - "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [missing-plugin]", + "Plugin \\"another-explicitly-disabled-plugin-preboot\\" is disabled.", ], Array [ - "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [another-explicitly-disabled-plugin]", + "Plugin \\"another-explicitly-disabled-plugin-standard\\" is disabled.", ], Array [ - "Plugin \\"another-explicitly-disabled-plugin\\" is disabled.", + "Plugin \\"plugin-with-disabled-nested-transitive-dep-preboot\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [plugin-with-disabled-transitive-dep-preboot]", ], Array [ - "Plugin \\"plugin-with-disabled-nested-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-disabled-transitive-dep]", + "Plugin \\"plugin-with-disabled-nested-transitive-dep-standard\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [plugin-with-disabled-transitive-dep-standard]", ], Array [ - "Plugin \\"plugin-with-missing-nested-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-missing-required-deps]", + "Plugin \\"plugin-with-missing-nested-dep-preboot\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [plugin-with-missing-required-deps-preboot]", + ], + Array [ + "Plugin \\"plugin-with-missing-nested-dep-standard\\" has been disabled since the following direct or transitive dependencies are missing, disabled, or have incompatible types: [plugin-with-missing-required-deps-standard]", ], ] `); }); it('does not throw in case of mutual plugin dependencies', async () => { - const firstPlugin = createPlugin('first-plugin', { - path: 'path-1', - requiredPlugins: ['second-plugin'], + const prebootPlugins = [ + createPlugin('first-plugin-preboot', { + type: PluginType.preboot, + path: 'path-1-preboot', + requiredPlugins: ['second-plugin-preboot'], + }), + createPlugin('second-plugin-preboot', { + type: PluginType.preboot, + path: 'path-2-preboot', + requiredPlugins: ['first-plugin-preboot'], + }), + ]; + const standardPlugins = [ + createPlugin('first-plugin-standard', { + path: 'path-1-standard', + requiredPlugins: ['second-plugin-standard'], + }), + createPlugin('second-plugin-standard', { + path: 'path-2-standard', + requiredPlugins: ['first-plugin-standard'], + }), + ]; + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([...prebootPlugins, ...standardPlugins]), }); - const secondPlugin = createPlugin('second-plugin', { - path: 'path-2', - requiredPlugins: ['first-plugin'], + + const { preboot, standard } = await pluginsService.discover({ + environment: environmentPreboot, }); + expect(mockDiscover).toHaveBeenCalledTimes(1); + + expect(preboot.pluginTree).toBeUndefined(); + for (const plugin of prebootPlugins) { + expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin); + } + expect(standard.pluginTree).toBeUndefined(); + for (const plugin of standardPlugins) { + expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin); + } + }); + + it('does not throw in case of mutual plugin dependencies between preboot and standard plugins', async () => { mockDiscover.mockReturnValue({ error$: from([]), - plugin$: from([firstPlugin, secondPlugin]), + plugin$: from([ + createPlugin('first-plugin-preboot', { + type: PluginType.preboot, + path: 'path-1-preboot', + requiredPlugins: ['second-plugin-standard'], + }), + createPlugin('first-plugin-standard', { + path: 'path-1-standard', + requiredPlugins: ['second-plugin-preboot'], + }), + createPlugin('second-plugin-preboot', { + type: PluginType.preboot, + path: 'path-2-preboot', + requiredPlugins: ['first-plugin-standard'], + }), + createPlugin('second-plugin-standard', { + path: 'path-2-standard', + requiredPlugins: ['first-plugin-preboot'], + }), + ]), }); - const { pluginTree } = await pluginsService.discover({ environment: environmentSetup }); - expect(pluginTree).toBeUndefined(); + const { preboot, standard } = await pluginsService.discover({ + environment: environmentPreboot, + }); + expect(preboot.pluginTree).toBeUndefined(); + expect(standard.pluginTree).toBeUndefined(); expect(mockDiscover).toHaveBeenCalledTimes(1); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); + expect(prebootMockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(standardMockPluginSystem.addPlugin).not.toHaveBeenCalled(); }); it('does not throw in case of cyclic plugin dependencies', async () => { - const firstPlugin = createPlugin('first-plugin', { - path: 'path-1', - requiredPlugins: ['second-plugin'], - }); - const secondPlugin = createPlugin('second-plugin', { - path: 'path-2', - requiredPlugins: ['third-plugin', 'last-plugin'], - }); - const thirdPlugin = createPlugin('third-plugin', { - path: 'path-3', - requiredPlugins: ['last-plugin', 'first-plugin'], - }); - const lastPlugin = createPlugin('last-plugin', { - path: 'path-4', - requiredPlugins: ['first-plugin'], - }); - const missingDepsPlugin = createPlugin('missing-deps-plugin', { - path: 'path-5', - requiredPlugins: ['not-a-plugin'], - }); + const prebootPlugins = [ + createPlugin('first-plugin-preboot', { + type: PluginType.preboot, + path: 'path-1-preboot', + requiredPlugins: ['second-plugin-preboot'], + }), + createPlugin('second-plugin-preboot', { + type: PluginType.preboot, + path: 'path-2-preboot', + requiredPlugins: ['third-plugin-preboot', 'last-plugin-preboot'], + }), + createPlugin('third-plugin-preboot', { + type: PluginType.preboot, + path: 'path-3-preboot', + requiredPlugins: ['last-plugin-preboot', 'first-plugin-preboot'], + }), + createPlugin('last-plugin-preboot', { + type: PluginType.preboot, + path: 'path-4-preboot', + requiredPlugins: ['first-plugin-preboot'], + }), + createPlugin('missing-deps-plugin-preboot', { + type: PluginType.preboot, + path: 'path-5-preboot', + requiredPlugins: ['not-a-plugin-preboot'], + }), + ]; + + const standardPlugins = [ + createPlugin('first-plugin-standard', { + path: 'path-1-standard', + requiredPlugins: ['second-plugin-standard'], + }), + createPlugin('second-plugin-standard', { + path: 'path-2-standard', + requiredPlugins: ['third-plugin-standard', 'last-plugin-standard'], + }), + createPlugin('third-plugin-standard', { + path: 'path-3-standard', + requiredPlugins: ['last-plugin-standard', 'first-plugin-standard'], + }), + createPlugin('last-plugin-standard', { + path: 'path-4-standard', + requiredPlugins: ['first-plugin-standard'], + }), + createPlugin('missing-deps-plugin-standard', { + path: 'path-5-standard', + requiredPlugins: ['not-a-plugin-standard'], + }), + ]; mockDiscover.mockReturnValue({ error$: from([]), - plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]), + plugin$: from([...prebootPlugins, ...standardPlugins]), }); - const { pluginTree } = await pluginsService.discover({ environment: environmentSetup }); - expect(pluginTree).toBeUndefined(); - + const { standard, preboot } = await pluginsService.discover({ + environment: environmentPreboot, + }); expect(mockDiscover).toHaveBeenCalledTimes(1); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(4); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(thirdPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(lastPlugin); + + expect(preboot.pluginTree).toBeUndefined(); + expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledTimes(4); + for (const plugin of prebootPlugins) { + if (plugin.name.startsWith('missing-deps')) { + expect(prebootMockPluginSystem.addPlugin).not.toHaveBeenCalledWith(plugin); + } else { + expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin); + } + } + + expect(standard.pluginTree).toBeUndefined(); + expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledTimes(4); + for (const plugin of standardPlugins) { + if (plugin.name.startsWith('missing-deps')) { + expect(standardMockPluginSystem.addPlugin).not.toHaveBeenCalledWith(plugin); + } else { + expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin); + } + } }); it('properly invokes plugin discovery and ignores non-critical errors.', async () => { - const firstPlugin = createPlugin('some-id', { - path: 'path-1', - configPath: 'path', - requiredPlugins: ['some-other-id'], - optionalPlugins: ['missing-optional-dep'], - }); - const secondPlugin = createPlugin('some-other-id', { - path: 'path-2', - version: 'some-other-version', - configPath: ['plugin', 'path'], - }); + const prebootPlugins = [ + createPlugin('some-id-preboot', { + type: PluginType.preboot, + path: 'path-1-preboot', + configPath: 'path-preboot', + requiredPlugins: ['some-other-id-preboot'], + optionalPlugins: ['missing-optional-dep'], + }), + createPlugin('some-other-id-preboot', { + type: PluginType.preboot, + path: 'path-2-preboot', + version: 'some-other-version', + configPath: ['plugin-other-preboot', 'path'], + }), + ]; + + const standardPlugins = [ + createPlugin('some-id-standard', { + type: PluginType.standard, + path: 'path-1-standard', + configPath: 'path-standard', + requiredPlugins: ['some-other-id-standard'], + optionalPlugins: ['missing-optional-dep'], + }), + createPlugin('some-other-id-standard', { + type: PluginType.standard, + path: 'path-2-standard', + version: 'some-other-version', + configPath: ['plugin-other-standard', 'path'], + }), + ]; mockDiscover.mockReturnValue({ error$: from([ @@ -367,13 +609,20 @@ describe('PluginsService', () => { PluginDiscoveryError.invalidSearchPath('dir-1', new Error('No dir')), PluginDiscoveryError.invalidPluginPath('path4-1', new Error('No path')), ]), - plugin$: from([firstPlugin, secondPlugin]), + plugin$: from([...prebootPlugins, ...standardPlugins]), }); - await pluginsService.discover({ environment: environmentSetup }); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); + await pluginsService.discover({ environment: environmentPreboot }); + expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); + for (const plugin of prebootPlugins) { + expect(prebootMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin); + } + + expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); + for (const plugin of standardPlugins) { + expect(standardMockPluginSystem.addPlugin).toHaveBeenCalledWith(plugin); + } + expect(mockDiscover).toHaveBeenCalledTimes(1); expect(mockDiscover).toHaveBeenCalledWith( { @@ -399,27 +648,33 @@ describe('PluginsService', () => { const configSchema = schema.string(); jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); jest.doMock( - join('path-with-schema', 'server'), - () => ({ - config: { - schema: configSchema, - }, - }), - { - virtual: true, - } + join('path-with-schema-preboot', 'server'), + () => ({ config: { schema: configSchema } }), + { virtual: true } + ); + jest.doMock( + join('path-with-schema-standard', 'server'), + () => ({ config: { schema: configSchema } }), + { virtual: true } ); + mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - createPlugin('some-id', { - path: 'path-with-schema', - configPath: 'path', + createPlugin('some-id-preboot', { + type: PluginType.preboot, + path: 'path-with-schema-preboot', + configPath: 'path-preboot', + }), + createPlugin('some-id-standard', { + path: 'path-with-schema-standard', + configPath: 'path-standard', }), ]), }); - await pluginsService.discover({ environment: environmentSetup }); - expect(configService.setSchema).toBeCalledWith('path', configSchema); + await pluginsService.discover({ environment: environmentPreboot }); + expect(configService.setSchema).toBeCalledWith('path-preboot', configSchema); + expect(configService.setSchema).toBeCalledWith('path-standard', configSchema); }); it('registers plugin config deprecation provider in config service', async () => { @@ -427,127 +682,183 @@ describe('PluginsService', () => { jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); jest.spyOn(configService, 'addDeprecationProvider'); - const deprecationProvider = () => []; + const prebootDeprecationProvider = () => []; jest.doMock( - join('path-with-provider', 'server'), - () => ({ - config: { - schema: configSchema, - deprecations: deprecationProvider, - }, - }), - { - virtual: true, - } + join('path-with-provider-preboot', 'server'), + () => ({ config: { schema: configSchema, deprecations: prebootDeprecationProvider } }), + { virtual: true } + ); + + const standardDeprecationProvider = () => []; + jest.doMock( + join('path-with-provider-standard', 'server'), + () => ({ config: { schema: configSchema, deprecations: standardDeprecationProvider } }), + { virtual: true } ); + mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - createPlugin('some-id', { - path: 'path-with-provider', - configPath: 'config-path', + createPlugin('some-id-preboot', { + type: PluginType.preboot, + path: 'path-with-provider-preboot', + configPath: 'config-path-preboot', + }), + createPlugin('some-id-standard', { + path: 'path-with-provider-standard', + configPath: 'config-path-standard', }), ]), }); - await pluginsService.discover({ environment: environmentSetup }); + await pluginsService.discover({ environment: environmentPreboot }); + expect(configService.addDeprecationProvider).toBeCalledWith( + 'config-path-preboot', + prebootDeprecationProvider + ); expect(configService.addDeprecationProvider).toBeCalledWith( - 'config-path', - deprecationProvider + 'config-path-standard', + standardDeprecationProvider ); }); it('returns the paths of the plugins', async () => { - const pluginA = createPlugin('A', { path: '/plugin-A-path', configPath: 'pathA' }); - const pluginB = createPlugin('B', { path: '/plugin-B-path', configPath: 'pathB' }); - mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([]), }); - mockPluginSystem.getPlugins.mockReturnValue([pluginA, pluginB]); - - const { pluginPaths } = await pluginsService.discover({ environment: environmentSetup }); + prebootMockPluginSystem.getPlugins.mockImplementation(() => [ + createPlugin('A-preboot', { + type: PluginType.preboot, + path: '/plugin-A-path-preboot', + configPath: 'pathA-preboot', + }), + createPlugin('B-preboot', { + type: PluginType.preboot, + path: '/plugin-B-path-preboot', + configPath: 'pathB-preboot', + }), + ]); - expect(pluginPaths).toEqual(['/plugin-A-path', '/plugin-B-path']); - }); + standardMockPluginSystem.getPlugins.mockImplementation(() => [ + createPlugin('A-standard', { + path: '/plugin-A-path-standard', + configPath: 'pathA-standard', + }), + createPlugin('B-standard', { + path: '/plugin-B-path-standard', + configPath: 'pathB-standard', + }), + ]); - it('ppopulates pluginConfigUsageDescriptors with plugins exposeToUsage property', async () => { - const pluginA = createPlugin('plugin-with-expose-usage', { - path: 'plugin-with-expose-usage', - configPath: 'pathA', + const { preboot, standard } = await pluginsService.discover({ + environment: environmentPreboot, }); - jest.doMock( - join('plugin-with-expose-usage', 'server'), - () => ({ - config: { - exposeToUsage: { - test: true, - nested: { - prop: true, - }, + expect(preboot.pluginPaths).toEqual(['/plugin-A-path-preboot', '/plugin-B-path-preboot']); + expect(standard.pluginPaths).toEqual(['/plugin-A-path-standard', '/plugin-B-path-standard']); + }); + + it('populates pluginConfigUsageDescriptors with plugins exposeToUsage property', async () => { + const pluginsWithExposeUsage = [ + createPlugin('plugin-with-expose-usage-preboot', { + type: PluginType.preboot, + path: 'plugin-with-expose-usage-preboot', + configPath: 'pathA-preboot', + }), + createPlugin('plugin-with-expose-usage-standard', { + path: 'plugin-with-expose-usage-standard', + configPath: 'pathA-standard', + }), + ]; + for (const plugin of pluginsWithExposeUsage) { + jest.doMock( + join(plugin.path, 'server'), + () => ({ + config: { + exposeToUsage: { test: true, nested: { prop: true } }, + schema: schema.maybe(schema.any()), }, - schema: schema.maybe(schema.any()), - }, + }), + { virtual: true } + ); + } + + const pluginsWithArrayConfigPath = [ + createPlugin('plugin-with-array-configPath-preboot', { + type: PluginType.preboot, + path: 'plugin-with-array-configPath-preboot', + version: 'some-other-version', + configPath: ['plugin-preboot', 'pathB'], }), - { - virtual: true, - } - ); - - const pluginB = createPlugin('plugin-with-array-configPath', { - path: 'plugin-with-array-configPath', - configPath: ['plugin', 'pathB'], - }); - - jest.doMock( - join('plugin-with-array-configPath', 'server'), - () => ({ - config: { - exposeToUsage: { - test: true, + createPlugin('plugin-with-array-configPath-standard', { + path: 'plugin-with-array-configPath-standard', + version: 'some-other-version', + configPath: ['plugin-standard', 'pathB'], + }), + ]; + for (const plugin of pluginsWithArrayConfigPath) { + jest.doMock( + join(plugin.path, 'server'), + () => ({ + config: { + exposeToUsage: { test: true }, + schema: schema.maybe(schema.any()), }, - schema: schema.maybe(schema.any()), - }, + }), + { virtual: true } + ); + } + + const pluginsWithoutExpose = [ + createPlugin('plugin-without-expose-preboot', { + type: PluginType.preboot, + path: 'plugin-without-expose-preboot', + configPath: 'pathC-preboot', }), - { - virtual: true, - } - ); - - jest.doMock( - join('plugin-without-expose', 'server'), - () => ({ - config: { - schema: schema.maybe(schema.any()), - }, + createPlugin('plugin-without-expose-standard', { + path: 'plugin-without-expose-standard', + configPath: 'pathC-standard', }), - { - virtual: true, - } - ); - - const pluginC = createPlugin('plugin-without-expose', { - path: 'plugin-without-expose', - configPath: 'pathC', - }); + ]; + for (const plugin of pluginsWithoutExpose) { + jest.doMock( + join(plugin.path, 'server'), + () => ({ + config: { + schema: schema.maybe(schema.any()), + }, + }), + { virtual: true } + ); + } mockDiscover.mockReturnValue({ error$: from([]), - plugin$: from([pluginA, pluginB, pluginC]), + plugin$: from([ + ...pluginsWithExposeUsage, + ...pluginsWithArrayConfigPath, + ...pluginsWithoutExpose, + ]), }); - await pluginsService.discover({ environment: environmentSetup }); + await pluginsService.discover({ environment: environmentPreboot }); // eslint-disable-next-line dot-notation expect(pluginsService['pluginConfigUsageDescriptors']).toMatchInlineSnapshot(` Map { - "pathA" => Object { + "pathA-preboot" => Object { "nested.prop": true, "test": true, }, - "plugin.pathB" => Object { + "pathA-standard" => Object { + "nested.prop": true, + "test": true, + }, + "plugin-preboot.pathB" => Object { + "test": true, + }, + "plugin-standard.pathB" => Object { "test": true, }, } @@ -560,6 +871,7 @@ describe('PluginsService', () => { plugin.name, { id: plugin.name, + type: plugin.manifest.type, configPath: plugin.manifest.configPath, requiredPlugins: [], requiredBundles: [], @@ -568,140 +880,246 @@ describe('PluginsService', () => { ]; it('properly generates client configs for plugins according to `exposeToBrowser`', async () => { - jest.doMock( - join('plugin-with-expose', 'server'), - () => ({ - config: { - exposeToBrowser: { - sharedProp: true, - }, - schema: schema.object({ - serverProp: schema.string({ defaultValue: 'serverProp default value' }), - sharedProp: schema.string({ defaultValue: 'sharedProp default value' }), - }), - }, - }), - { - virtual: true, - } - ); - const plugin = createPlugin('plugin-with-expose', { - path: 'plugin-with-expose', - configPath: 'path', + const prebootPlugin = createPlugin('plugin-with-expose-preboot', { + type: PluginType.preboot, + path: 'plugin-with-expose-preboot', + configPath: 'path-preboot', }); + const standardPlugin = createPlugin('plugin-with-expose-standard', { + path: 'plugin-with-expose-standard', + configPath: 'path-standard', + }); + for (const plugin of [prebootPlugin, standardPlugin]) { + jest.doMock( + join(plugin.path, 'server'), + () => ({ + config: { + exposeToBrowser: { + sharedProp: true, + }, + schema: schema.object({ + serverProp: schema.string({ + defaultValue: `serverProp default value ${plugin.name}`, + }), + sharedProp: schema.string({ + defaultValue: `sharedProp default value ${plugin.name}`, + }), + }), + }, + }), + { virtual: true } + ); + } + mockDiscover.mockReturnValue({ error$: from([]), - plugin$: from([plugin]), + plugin$: from([prebootPlugin, standardPlugin]), + }); + prebootMockPluginSystem.uiPlugins.mockReturnValue( + new Map([pluginToDiscoveredEntry(prebootPlugin)]) + ); + standardMockPluginSystem.uiPlugins.mockReturnValue( + new Map([pluginToDiscoveredEntry(standardPlugin)]) + ); + + const { preboot, standard } = await pluginsService.discover({ + environment: environmentPreboot, }); - mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); - const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); - const uiConfig$ = uiPlugins.browserConfigs.get('plugin-with-expose'); - expect(uiConfig$).toBeDefined(); + const prebootUIConfig$ = preboot.uiPlugins.browserConfigs.get('plugin-with-expose-preboot')!; + await expect(prebootUIConfig$.pipe(take(1)).toPromise()).resolves.toEqual({ + sharedProp: 'sharedProp default value plugin-with-expose-preboot', + }); - const uiConfig = await uiConfig$!.pipe(take(1)).toPromise(); - expect(uiConfig).toMatchInlineSnapshot(` - Object { - "sharedProp": "sharedProp default value", - } - `); + const standardUIConfig$ = standard.uiPlugins.browserConfigs.get( + 'plugin-with-expose-standard' + )!; + await expect(standardUIConfig$.pipe(take(1)).toPromise()).resolves.toEqual({ + sharedProp: 'sharedProp default value plugin-with-expose-standard', + }); }); it('does not generate config for plugins not exposing to client', async () => { - jest.doMock( - join('plugin-without-expose', 'server'), - () => ({ - config: { - schema: schema.object({ - serverProp: schema.string({ defaultValue: 'serverProp default value' }), - }), - }, - }), - { - virtual: true, - } - ); - const plugin = createPlugin('plugin-without-expose', { - path: 'plugin-without-expose', - configPath: 'path', + const prebootPlugin = createPlugin('plugin-without-expose-preboot', { + type: PluginType.preboot, + path: 'plugin-without-expose-preboot', + configPath: 'path-preboot', + }); + const standardPlugin = createPlugin('plugin-without-expose-standard', { + path: 'plugin-without-expose-standard', + configPath: 'path-standard', }); + for (const plugin of [prebootPlugin, standardPlugin]) { + jest.doMock( + join(plugin.path, 'server'), + () => ({ + config: { + schema: schema.object({ + serverProp: schema.string({ defaultValue: 'serverProp default value' }), + }), + }, + }), + { virtual: true } + ); + } + mockDiscover.mockReturnValue({ error$: from([]), - plugin$: from([plugin]), + plugin$: from([prebootPlugin, standardPlugin]), }); - mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); + prebootMockPluginSystem.uiPlugins.mockReturnValue( + new Map([pluginToDiscoveredEntry(prebootPlugin)]) + ); + standardMockPluginSystem.uiPlugins.mockReturnValue( + new Map([pluginToDiscoveredEntry(standardPlugin)]) + ); - const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); - expect([...uiPlugins.browserConfigs.entries()]).toHaveLength(0); + const { preboot, standard } = await pluginsService.discover({ + environment: environmentPreboot, + }); + expect(preboot.uiPlugins.browserConfigs.size).toBe(0); + expect(standard.uiPlugins.browserConfigs.size).toBe(0); }); }); - describe('#setup()', () => { + describe('plugin initialization', () => { beforeEach(() => { mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - createPlugin('plugin-1', { - path: 'path-1', + createPlugin('plugin-1-preboot', { + type: PluginType.preboot, + path: 'path-1-preboot', version: 'version-1', - configPath: 'plugin1', + configPath: 'plugin1_preboot', }), - createPlugin('plugin-2', { - path: 'path-2', + createPlugin('plugin-1-standard', { + path: 'path-1-standard', + version: 'version-1', + configPath: 'plugin1_standard', + }), + createPlugin('plugin-2-preboot', { + type: PluginType.preboot, + path: 'path-2-preboot', version: 'version-2', - configPath: 'plugin2', + configPath: 'plugin2_preboot', + }), + createPlugin('plugin-2-standard', { + path: 'path-2-standard', + version: 'version-2', + configPath: 'plugin2_standard', }), ]), }); - mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + prebootMockPluginSystem.uiPlugins.mockReturnValue(new Map()); + standardMockPluginSystem.uiPlugins.mockReturnValue(new Map()); }); - describe('uiPlugins.internal', () => { - it('contains internal properties for plugins', async () => { - config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); - const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); - expect(uiPlugins.internal).toMatchInlineSnapshot(` - Map { - "plugin-1" => Object { - "publicAssetsDir": /path-1/public/assets, - "publicTargetDir": /path-1/target/public, - "requiredBundles": Array [], - "version": "version-1", - }, - "plugin-2" => Object { - "publicAssetsDir": /path-2/public/assets, - "publicTargetDir": /path-2/target/public, - "requiredBundles": Array [], - "version": "version-2", - }, - } - `); + it('`uiPlugins.internal` contains internal properties for plugins', async () => { + config$.next({ + plugins: { initialize: true }, + plugin1_preboot: { enabled: false }, + plugin1_standard: { enabled: false }, }); - - it('includes disabled plugins', async () => { - config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); - const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); - expect([...uiPlugins.internal.keys()].sort()).toEqual(['plugin-1', 'plugin-2']); + const { preboot, standard } = await pluginsService.discover({ + environment: environmentPreboot, }); + expect(preboot.uiPlugins.internal).toMatchInlineSnapshot(` + Map { + "plugin-1-preboot" => Object { + "publicAssetsDir": /path-1-preboot/public/assets, + "publicTargetDir": /path-1-preboot/target/public, + "requiredBundles": Array [], + "version": "version-1", + }, + "plugin-2-preboot" => Object { + "publicAssetsDir": /path-2-preboot/public/assets, + "publicTargetDir": /path-2-preboot/target/public, + "requiredBundles": Array [], + "version": "version-2", + }, + } + `); + expect(standard.uiPlugins.internal).toMatchInlineSnapshot(` + Map { + "plugin-1-standard" => Object { + "publicAssetsDir": /path-1-standard/public/assets, + "publicTargetDir": /path-1-standard/target/public, + "requiredBundles": Array [], + "version": "version-1", + }, + "plugin-2-standard" => Object { + "publicAssetsDir": /path-2-standard/public/assets, + "publicTargetDir": /path-2-standard/target/public, + "requiredBundles": Array [], + "version": "version-2", + }, + } + `); }); - describe('plugin initialization', () => { - it('does initialize if plugins.initialize is true', async () => { - config$.next({ plugins: { initialize: true } }); - await pluginsService.discover({ environment: environmentSetup }); - const { initialized } = await pluginsService.setup(setupDeps); - expect(mockPluginSystem.setupPlugins).toHaveBeenCalled(); - expect(initialized).toBe(true); + it('`uiPlugins.internal` includes disabled plugins', async () => { + config$.next({ + plugins: { initialize: true }, + plugin1_preboot: { enabled: false }, + plugin1_standard: { enabled: false }, }); - - it('does not initialize if plugins.initialize is false', async () => { - config$.next({ plugins: { initialize: false } }); - await pluginsService.discover({ environment: environmentSetup }); - const { initialized } = await pluginsService.setup(setupDeps); - expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled(); - expect(initialized).toBe(false); + const { preboot, standard } = await pluginsService.discover({ + environment: environmentPreboot, }); + expect([...preboot.uiPlugins.internal.keys()].sort()).toMatchInlineSnapshot(` + Array [ + "plugin-1-preboot", + "plugin-2-preboot", + ] + `); + expect([...standard.uiPlugins.internal.keys()].sort()).toMatchInlineSnapshot(` + Array [ + "plugin-1-standard", + "plugin-2-standard", + ] + `); + }); + + it('#preboot does initialize `preboot` plugins if plugins.initialize is true', async () => { + config$.next({ plugins: { initialize: true } }); + await pluginsService.discover({ environment: environmentPreboot }); + await pluginsService.preboot(prebootDeps); + + expect(prebootMockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); + expect(prebootMockPluginSystem.setupPlugins).toHaveBeenCalledWith(prebootDeps); + expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + }); + + it('#preboot does not initialize `preboot` plugins if plugins.initialize is false', async () => { + config$.next({ plugins: { initialize: false } }); + await pluginsService.discover({ environment: environmentPreboot }); + await pluginsService.preboot(prebootDeps); + + expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + }); + + it('#setup does initialize `standard` plugins if plugins.initialize is true', async () => { + config$.next({ plugins: { initialize: true } }); + await pluginsService.discover({ environment: environmentPreboot }); + await pluginsService.preboot(prebootDeps); + + const { initialized } = await pluginsService.setup(setupDeps); + expect(standardMockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); + expect(standardMockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); + expect(initialized).toBe(true); + }); + + it('#setup does not initialize `standard` plugins if plugins.initialize is false', async () => { + config$.next({ plugins: { initialize: false } }); + await pluginsService.discover({ environment: environmentPreboot }); + await pluginsService.preboot(prebootDeps); + const { initialized } = await pluginsService.setup(setupDeps); + expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + expect(initialized).toBe(false); }); }); @@ -719,10 +1137,74 @@ describe('PluginsService', () => { }); }); + describe('#start()', () => { + beforeEach(() => { + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('plugin-1-preboot', { type: PluginType.preboot, path: 'path-1-preboot' }), + createPlugin('plugin-1-standard', { path: 'path-1-standard' }), + ]), + }); + }); + + it('does not try to stop `preboot` plugins and start `standard` ones if plugins.initialize is `false`', async () => { + config$.next({ plugins: { initialize: false } }); + + await pluginsService.discover({ environment: environmentPreboot }); + await pluginsService.preboot(prebootDeps); + await pluginsService.setup(setupDeps); + + const { contracts } = await pluginsService.start(startDeps); + expect(contracts).toBeInstanceOf(Map); + expect(contracts.size).toBe(0); + + expect(prebootMockPluginSystem.stopPlugins).not.toHaveBeenCalled(); + expect(standardMockPluginSystem.startPlugins).not.toHaveBeenCalled(); + }); + + it('stops `preboot` plugins and starts `standard` ones', async () => { + await pluginsService.discover({ environment: environmentPreboot }); + await pluginsService.preboot(prebootDeps); + await pluginsService.setup(setupDeps); + + expect(prebootMockPluginSystem.stopPlugins).not.toHaveBeenCalled(); + expect(standardMockPluginSystem.startPlugins).not.toHaveBeenCalled(); + + await pluginsService.start(startDeps); + + expect(prebootMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); + expect(standardMockPluginSystem.stopPlugins).not.toHaveBeenCalled(); + + expect(standardMockPluginSystem.startPlugins).toHaveBeenCalledTimes(1); + expect(standardMockPluginSystem.startPlugins).toHaveBeenCalledWith(startDeps); + expect(prebootMockPluginSystem.startPlugins).not.toHaveBeenCalled(); + }); + }); + describe('#stop()', () => { it('`stop` stops plugins system', async () => { await pluginsService.stop(); - expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); + expect(standardMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); + expect(prebootMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); + }); + + it('`stop` does not try to stop preboot plugins system if it was stopped during `start`.', async () => { + await pluginsService.preboot(prebootDeps); + await pluginsService.setup(setupDeps); + + expect(prebootMockPluginSystem.stopPlugins).not.toHaveBeenCalled(); + expect(standardMockPluginSystem.stopPlugins).not.toHaveBeenCalled(); + + await pluginsService.start(startDeps); + + expect(prebootMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); + expect(standardMockPluginSystem.stopPlugins).not.toHaveBeenCalled(); + + await pluginsService.stop(); + + expect(prebootMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); + expect(standardMockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 99a9aaaddcb0b..05bb60fb22c6d 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -8,20 +8,36 @@ import Path from 'path'; import { Observable } from 'rxjs'; -import { filter, first, map, concatMap, tap, toArray } from 'rxjs/operators'; -import { pick, getFlattenedObject } from '@kbn/std'; +import { concatMap, filter, first, map, tap, toArray } from 'rxjs/operators'; +import { getFlattenedObject, pick } from '@kbn/std'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, PluginConfigDescriptor, PluginName, InternalPluginInfo } from './types'; +import { + DiscoveredPlugin, + InternalPluginInfo, + PluginConfigDescriptor, + PluginDependencies, + PluginName, + PluginType, +} from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; -import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; +import { InternalCorePreboot, InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { IConfigService } from '../config'; -import { InternalEnvironmentServiceSetup } from '../environment'; +import { InternalEnvironmentServicePreboot } from '../environment'; + +/** @internal */ +export type DiscoveredPlugins = { + [key in PluginType]: { + pluginTree: PluginDependencies; + pluginPaths: string[]; + uiPlugins: UiPlugins; + }; +}; /** @internal */ export interface PluginsServiceSetup { @@ -56,6 +72,9 @@ export interface PluginsServiceStart { contracts: Map; } +/** @internal */ +export type PluginsServicePrebootSetupDeps = InternalCorePreboot; + /** @internal */ export type PluginsServiceSetupDeps = InternalCoreSetup; @@ -64,29 +83,31 @@ export type PluginsServiceStartDeps = InternalCoreStart; /** @internal */ export interface PluginsServiceDiscoverDeps { - environment: InternalEnvironmentServiceSetup; + environment: InternalEnvironmentServicePreboot; } /** @internal */ export class PluginsService implements CoreService { private readonly log: Logger; - private readonly pluginsSystem: PluginsSystem; + private readonly prebootPluginsSystem = new PluginsSystem(this.coreContext, PluginType.preboot); + private arePrebootPluginsStopped = false; + private readonly prebootUiPluginInternalInfo = new Map(); + private readonly standardPluginsSystem = new PluginsSystem(this.coreContext, PluginType.standard); + private readonly standardUiPluginInternalInfo = new Map(); private readonly configService: IConfigService; private readonly config$: Observable; private readonly pluginConfigDescriptors = new Map(); - private readonly uiPluginInternalInfo = new Map(); private readonly pluginConfigUsageDescriptors = new Map>(); constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('plugins-service'); - this.pluginsSystem = new PluginsSystem(coreContext); this.configService = coreContext.configService; this.config$ = coreContext.configService .atPath('plugins') .pipe(map((rawConfig) => new PluginsConfig(rawConfig, coreContext.env))); } - public async discover({ environment }: PluginsServiceDiscoverDeps) { + public async discover({ environment }: PluginsServiceDiscoverDeps): Promise { const config = await this.config$.pipe(first()).toPromise(); const { error$, plugin$ } = discover(config, this.coreContext, { @@ -96,16 +117,26 @@ export class PluginsService implements CoreService plugin.path), - uiPlugins: { - internal: this.uiPluginInternalInfo, - public: uiPlugins, - browserConfigs: this.generateUiPluginsConfigs(uiPlugins), + preboot: { + pluginPaths: this.prebootPluginsSystem.getPlugins().map((plugin) => plugin.path), + pluginTree: this.prebootPluginsSystem.getPluginDependencies(), + uiPlugins: { + internal: this.prebootUiPluginInternalInfo, + public: prebootUiPlugins, + browserConfigs: this.generateUiPluginsConfigs(prebootUiPlugins), + }, + }, + standard: { + pluginPaths: this.standardPluginsSystem.getPlugins().map((plugin) => plugin.path), + pluginTree: this.standardPluginsSystem.getPluginDependencies(), + uiPlugins: { + internal: this.standardUiPluginInternalInfo, + public: standardUiPlugins, + browserConfigs: this.generateUiPluginsConfigs(standardUiPlugins), + }, }, }; } @@ -114,6 +145,20 @@ export class PluginsService implements CoreService(); if (config.initialize) { - contracts = await this.pluginsSystem.setupPlugins(deps); - this.registerPluginStaticDirs(deps); + contracts = await this.standardPluginsSystem.setupPlugins(deps); + this.registerPluginStaticDirs(deps, this.standardUiPluginInternalInfo); } else { - this.log.info('Plugin initialization disabled.'); + this.log.info( + 'Skipping `setup` for `standard` plugins since plugin initialization is disabled.' + ); } return { @@ -135,13 +182,31 @@ export class PluginsService implements CoreService, parents: PluginName[] = [] - ): { enabled: true } | { enabled: false; missingDependencies: string[] } { + ): { enabled: true } | { enabled: false; missingOrIncompatibleDependencies: string[] } { const pluginInfo = pluginEnableStatuses.get(pluginName); if (pluginInfo === undefined || !pluginInfo.isEnabled) { return { enabled: false, - missingDependencies: [], + missingOrIncompatibleDependencies: [], }; } - const missingDependencies = pluginInfo.plugin.requiredPlugins + const missingOrIncompatibleDependencies = pluginInfo.plugin.requiredPlugins .filter((dep) => !parents.includes(dep)) .filter( (dependencyName) => + pluginEnableStatuses.get(dependencyName)?.plugin.manifest.type !== + pluginInfo.plugin.manifest.type || !this.shouldEnablePlugin(dependencyName, pluginEnableStatuses, [...parents, pluginName]) .enabled ); - if (missingDependencies.length === 0) { + if (missingOrIncompatibleDependencies.length === 0) { return { enabled: true, }; @@ -308,12 +390,15 @@ export class PluginsService implements CoreService + ) { + for (const [pluginName, pluginInfo] of uiPluginInternalInfo) { deps.http.registerStaticDir( `/plugins/${pluginName}/assets/{path*}`, pluginInfo.publicAssetsDir diff --git a/src/core/server/plugins/plugins_system.test.mocks.ts b/src/core/server/plugins/plugins_system.test.mocks.ts index 26175ab2b68a9..1e1264ba76a80 100644 --- a/src/core/server/plugins/plugins_system.test.mocks.ts +++ b/src/core/server/plugins/plugins_system.test.mocks.ts @@ -6,9 +6,11 @@ * Side Public License, v 1. */ +export const mockCreatePluginPrebootSetupContext = jest.fn(); export const mockCreatePluginSetupContext = jest.fn(); export const mockCreatePluginStartContext = jest.fn(); jest.mock('./plugin_context', () => ({ + createPluginPrebootSetupContext: mockCreatePluginPrebootSetupContext, createPluginSetupContext: mockCreatePluginSetupContext, createPluginStartContext: mockCreatePluginStartContext, })); diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index abcd00f4e2daf..e61c9c2002a12 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -7,6 +7,7 @@ */ import { + mockCreatePluginPrebootSetupContext, mockCreatePluginSetupContext, mockCreatePluginStartContext, } from './plugins_system.test.mocks'; @@ -20,7 +21,7 @@ import { CoreContext } from '../core_context'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { PluginWrapper } from './plugin'; -import { PluginName } from './types'; +import { PluginName, PluginType } from './types'; import { PluginsSystem } from './plugins_system'; import { coreMock } from '../mocks'; import { Logger } from '../logging'; @@ -32,7 +33,14 @@ function createPlugin( optional = [], server = true, ui = true, - }: { required?: string[]; optional?: string[]; server?: boolean; ui?: boolean } = {} + type = PluginType.standard, + }: { + required?: string[]; + optional?: string[]; + server?: boolean; + ui?: boolean; + type?: PluginType; + } = {} ): PluginWrapper { return new PluginWrapper({ path: 'some-path', @@ -41,6 +49,7 @@ function createPlugin( version: 'some-version', configPath: 'path', kibanaVersion: '7.0.0', + type, requiredPlugins: required, optionalPlugins: optional, requiredBundles: [], @@ -52,10 +61,11 @@ function createPlugin( }); } +const prebootDeps = coreMock.createInternalPreboot(); const setupDeps = coreMock.createInternalSetup(); const startDeps = coreMock.createInternalStart(); -let pluginsSystem: PluginsSystem; +let pluginsSystem: PluginsSystem; let configService: ReturnType; let logger: ReturnType; let env: Env; @@ -70,7 +80,7 @@ beforeEach(() => { coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; - pluginsSystem = new PluginsSystem(coreContext); + pluginsSystem = new PluginsSystem(coreContext, PluginType.standard); }); test('can be setup even without plugins', async () => { @@ -80,6 +90,26 @@ test('can be setup even without plugins', async () => { expect(pluginsSetup.size).toBe(0); }); +test('throws if adding plugin with incompatible type', () => { + const prebootPlugin = createPlugin('plugin-preboot', { type: PluginType.preboot }); + const standardPlugin = createPlugin('plugin-standard'); + + const prebootPluginSystem = new PluginsSystem(coreContext, PluginType.preboot); + const standardPluginSystem = new PluginsSystem(coreContext, PluginType.standard); + + prebootPluginSystem.addPlugin(prebootPlugin); + expect(() => prebootPluginSystem.addPlugin(standardPlugin)).toThrowErrorMatchingInlineSnapshot( + `"Cannot add plugin with type \\"standard\\" to plugin system with type \\"preboot\\"."` + ); + expect(prebootPluginSystem.getPlugins()).toEqual([prebootPlugin]); + + standardPluginSystem.addPlugin(standardPlugin); + expect(() => standardPluginSystem.addPlugin(prebootPlugin)).toThrowErrorMatchingInlineSnapshot( + `"Cannot add plugin with type \\"preboot\\" to plugin system with type \\"standard\\"."` + ); + expect(standardPluginSystem.getPlugins()).toEqual([standardPlugin]); +}); + test('getPlugins returns the list of plugins', () => { const pluginA = createPlugin('plugin-a'); const pluginB = createPlugin('plugin-b'); @@ -293,6 +323,83 @@ test('correctly orders plugins and returns exposed values for "setup" and "start } }); +test('correctly orders preboot plugins and returns exposed values for "setup"', async () => { + const prebootPluginSystem = new PluginsSystem(coreContext, PluginType.preboot); + const plugins = new Map([ + [ + createPlugin('order-4', { type: PluginType.preboot, required: ['order-2'] }), + { 'order-2': 'added-as-2' }, + ], + [createPlugin('order-0', { type: PluginType.preboot }), {}], + [ + createPlugin('order-2', { + type: PluginType.preboot, + required: ['order-1'], + optional: ['order-0'], + }), + { 'order-1': 'added-as-3', 'order-0': 'added-as-1' }, + ], + [ + createPlugin('order-1', { type: PluginType.preboot, required: ['order-0'] }), + { 'order-0': 'added-as-1' }, + ], + [ + createPlugin('order-3', { + type: PluginType.preboot, + required: ['order-2'], + optional: ['missing-dep'], + }), + { 'order-2': 'added-as-2' }, + ], + ] as Array<[PluginWrapper, Record]>); + + const setupContextMap = new Map(); + [...plugins.keys()].forEach((plugin, index) => { + jest.spyOn(plugin, 'setup').mockResolvedValue(`added-as-${index}`); + setupContextMap.set(plugin.name, `setup-for-${plugin.name}`); + prebootPluginSystem.addPlugin(plugin); + }); + + mockCreatePluginPrebootSetupContext.mockImplementation((context, deps, plugin) => + setupContextMap.get(plugin.name) + ); + + expect([...(await prebootPluginSystem.setupPlugins(prebootDeps))]).toMatchInlineSnapshot(` + Array [ + Array [ + "order-0", + "added-as-1", + ], + Array [ + "order-1", + "added-as-3", + ], + Array [ + "order-2", + "added-as-2", + ], + Array [ + "order-3", + "added-as-4", + ], + Array [ + "order-4", + "added-as-0", + ], + ] + `); + + for (const [plugin, deps] of plugins) { + expect(mockCreatePluginPrebootSetupContext).toHaveBeenCalledWith( + coreContext, + prebootDeps, + plugin + ); + expect(plugin.setup).toHaveBeenCalledTimes(1); + expect(plugin.setup).toHaveBeenCalledWith(setupContextMap.get(plugin.name), deps); + } +}); + test('`setupPlugins` only setups plugins that have server side', async () => { const firstPluginToRun = createPlugin('order-0'); const secondPluginNotToRun = createPlugin('order-not-run', { server: false }); @@ -399,6 +506,21 @@ test('can start without plugins', async () => { expect(pluginsStart.size).toBe(0); }); +test('cannot start preboot plugins', async () => { + const prebootPlugin = createPlugin('order-0', { type: PluginType.preboot }); + jest.spyOn(prebootPlugin, 'setup').mockResolvedValue({}); + jest.spyOn(prebootPlugin, 'start').mockResolvedValue({}); + + const prebootPluginSystem = new PluginsSystem(coreContext, PluginType.preboot); + prebootPluginSystem.addPlugin(prebootPlugin); + await prebootPluginSystem.setupPlugins(prebootDeps); + + await expect( + prebootPluginSystem.startPlugins(startDeps) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Preboot plugins cannot be started."`); + expect(prebootPlugin.start).not.toHaveBeenCalled(); +}); + test('`startPlugins` only starts plugins that were setup', async () => { const firstPluginToRun = createPlugin('order-0'); const secondPluginNotToRun = createPlugin('order-not-run', { server: false }); @@ -525,7 +647,7 @@ describe('asynchronous plugins', () => { }) ); coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; - pluginsSystem = new PluginsSystem(coreContext); + pluginsSystem = new PluginsSystem(coreContext, PluginType.standard); const syncPlugin = createPlugin('sync-plugin'); jest.spyOn(syncPlugin, 'setup').mockReturnValue('setup-sync'); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index f6327d4eabf43..4a156c5fbb976 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -10,25 +10,38 @@ import { withTimeout, isPromise } from '@kbn/std'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, PluginName } from './types'; -import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; -import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; -import { PluginDependencies } from '.'; +import { DiscoveredPlugin, PluginDependencies, PluginName, PluginType } from './types'; +import { + createPluginPrebootSetupContext, + createPluginSetupContext, + createPluginStartContext, +} from './plugin_context'; +import { + PluginsServicePrebootSetupDeps, + PluginsServiceSetupDeps, + PluginsServiceStartDeps, +} from './plugins_service'; const Sec = 1000; /** @internal */ -export class PluginsSystem { +export class PluginsSystem { private readonly plugins = new Map(); private readonly log: Logger; // `satup`, the past-tense version of the noun `setup`. private readonly satupPlugins: PluginName[] = []; - constructor(private readonly coreContext: CoreContext) { - this.log = coreContext.logger.get('plugins-system'); + constructor(private readonly coreContext: CoreContext, public readonly type: T) { + this.log = coreContext.logger.get('plugins-system', this.type); } public addPlugin(plugin: PluginWrapper) { + if (plugin.manifest.type !== this.type) { + throw new Error( + `Cannot add plugin with type "${plugin.manifest.type}" to plugin system with type "${this.type}".` + ); + } + this.plugins.set(plugin.name, plugin); } @@ -67,7 +80,9 @@ export class PluginsSystem { return { asNames, asOpaqueIds }; } - public async setupPlugins(deps: PluginsServiceSetupDeps) { + public async setupPlugins( + deps: T extends PluginType.preboot ? PluginsServicePrebootSetupDeps : PluginsServiceSetupDeps + ): Promise> { const contracts = new Map(); if (this.plugins.size === 0) { return contracts; @@ -95,11 +110,23 @@ export class PluginsSystem { return depContracts; }, {} as Record); + let pluginSetupContext; + if (this.type === PluginType.preboot) { + pluginSetupContext = createPluginPrebootSetupContext( + this.coreContext, + deps as PluginsServicePrebootSetupDeps, + plugin + ); + } else { + pluginSetupContext = createPluginSetupContext( + this.coreContext, + deps as PluginsServiceSetupDeps, + plugin + ); + } + let contract: unknown; - const contractOrPromise = plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ); + const contractOrPromise = plugin.setup(pluginSetupContext, pluginDepContracts); if (isPromise(contractOrPromise)) { if (this.coreContext.env.mode.dev) { this.log.warn( @@ -130,6 +157,10 @@ export class PluginsSystem { } public async startPlugins(deps: PluginsServiceStartDeps) { + if (this.type === PluginType.preboot) { + throw new Error('Preboot plugins cannot be started.'); + } + const contracts = new Map(); if (this.satupPlugins.length === 0) { return contracts; @@ -222,6 +253,7 @@ export class PluginsSystem { pluginName, { id: pluginName, + type: plugin.manifest.type, configPath: plugin.manifest.configPath, requiredPlugins: plugin.manifest.requiredPlugins.filter((p) => uiPluginNames.includes(p) diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 0cdc806e997ef..b0edcbdfd8677 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -16,7 +16,7 @@ import { LoggerFactory } from '../logging'; import { KibanaConfigType } from '../kibana_config'; import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config'; import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config'; -import { CoreSetup, CoreStart } from '..'; +import { CorePreboot, CoreSetup, CoreStart } from '..'; type Maybe = T | undefined; @@ -116,6 +116,18 @@ export type PluginName = string; /** @public */ export type PluginOpaqueId = symbol; +/** @public */ +export enum PluginType { + /** + * Preboot plugins are special-purpose plugins that only function during preboot stage. + */ + preboot = 'preboot', + /** + * Standard plugins are plugins that start to function as soon as Kibana is fully booted and are active until it shuts down. + */ + standard = 'standard', +} + /** @internal */ export interface PluginDependencies { asNames: ReadonlyMap; @@ -149,6 +161,11 @@ export interface PluginManifest { */ readonly kibanaVersion: string; + /** + * Type of the plugin, defaults to `standard`. + */ + readonly type: PluginType; + /** * Root {@link ConfigPath | configuration path} used by the plugin, defaults * to "id" in snake_case format. @@ -247,6 +264,11 @@ export interface DiscoveredPlugin { */ readonly configPath: ConfigPath; + /** + * Type of the plugin, defaults to `standard`. + */ + readonly type: PluginType; + /** * An optional list of the other plugins that **must be** installed and enabled * for this plugin to function properly. @@ -296,7 +318,18 @@ export interface InternalPluginInfo { } /** - * The interface that should be returned by a `PluginInitializer`. + * The interface that should be returned by a `PluginInitializer` for a `preboot` plugin. + * + * @public + */ +export interface PrebootPlugin { + setup(core: CorePreboot, plugins: TPluginsSetup): TSetup; + + stop?(): void; +} + +/** + * The interface that should be returned by a `PluginInitializer` for a `standard` plugin. * * @public */ @@ -361,6 +394,7 @@ export interface PluginInitializerContext { mode: EnvironmentMode; packageInfo: Readonly; instanceUuid: string; + configs: readonly string[]; }; /** * {@link LoggerFactory | logger factory} instance already bound to the plugin's logging context @@ -471,4 +505,5 @@ export type PluginInitializer< core: PluginInitializerContext ) => | Plugin + | PrebootPlugin | AsyncPlugin; diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/index.ts b/src/core/server/preboot/index.ts similarity index 72% rename from src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/index.ts rename to src/core/server/preboot/index.ts index 4f70f9d30b74b..2b7f25538dcb1 100644 --- a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/index.ts +++ b/src/core/server/preboot/index.ts @@ -6,4 +6,5 @@ * Side Public License, v 1. */ -export { strings as revealImage } from './reveal_image'; +export { PrebootService } from './preboot_service'; +export type { InternalPrebootServicePreboot, PrebootServicePreboot } from './types'; diff --git a/src/core/server/preboot/preboot_service.mock.ts b/src/core/server/preboot/preboot_service.mock.ts new file mode 100644 index 0000000000000..acdd9458a462d --- /dev/null +++ b/src/core/server/preboot/preboot_service.mock.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { InternalPrebootServicePreboot, PrebootServicePreboot } from './types'; +import { PrebootService } from './preboot_service'; + +export type InternalPrebootServicePrebootMock = jest.Mocked; +export type PrebootServicePrebootMock = jest.Mocked; + +const createInternalPrebootContractMock = () => { + const mock: InternalPrebootServicePrebootMock = { + isSetupOnHold: jest.fn(), + holdSetupUntilResolved: jest.fn(), + waitUntilCanSetup: jest.fn(), + }; + return mock; +}; + +const createPrebootContractMock = () => { + const mock: PrebootServicePrebootMock = { + isSetupOnHold: jest.fn(), + holdSetupUntilResolved: jest.fn(), + }; + + return mock; +}; + +type PrebootServiceContract = PublicMethodsOf; + +const createPrebootServiceMock = () => { + const mocked: jest.Mocked = { + preboot: jest.fn(), + stop: jest.fn(), + }; + mocked.preboot.mockReturnValue(createInternalPrebootContractMock()); + return mocked; +}; + +export const prebootServiceMock = { + create: createPrebootServiceMock, + createInternalPrebootContract: createInternalPrebootContractMock, + createPrebootContract: createPrebootContractMock, +}; diff --git a/src/core/server/preboot/preboot_service.test.ts b/src/core/server/preboot/preboot_service.test.ts new file mode 100644 index 0000000000000..dd4b1cb7d1df0 --- /dev/null +++ b/src/core/server/preboot/preboot_service.test.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { nextTick } from '@kbn/test/jest'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import { LoggerFactory } from '@kbn/logging'; +import { Env } from '@kbn/config'; +import { getEnvOptions } from '../config/mocks'; +import { configServiceMock, loggingSystemMock } from '../mocks'; + +import { PrebootService } from './preboot_service'; + +describe('PrebootService', () => { + describe('#preboot()', () => { + let service: PrebootService; + let logger: LoggerFactory; + beforeEach(() => { + logger = loggingSystemMock.create(); + service = new PrebootService({ + configService: configServiceMock.create(), + env: Env.createDefault(REPO_ROOT, getEnvOptions()), + logger, + coreId: Symbol(), + }); + }); + + it('returns a proper contract', () => { + expect(service.preboot()).toMatchInlineSnapshot(` + Object { + "holdSetupUntilResolved": [Function], + "isSetupOnHold": [Function], + "waitUntilCanSetup": [Function], + } + `); + }); + + it('#isSetupOnHold correctly determines if `setup` is on hold', async () => { + const preboot = service.preboot(); + + expect(preboot.isSetupOnHold()).toBe(false); + + let resolveFirstPromise: (value?: { shouldReloadConfig: boolean }) => void; + preboot.holdSetupUntilResolved( + 'some-plugin', + 'some-reason', + new Promise<{ shouldReloadConfig: boolean } | undefined>((resolve) => { + resolveFirstPromise = resolve; + }) + ); + + let resolveSecondPromise: (value?: { shouldReloadConfig: boolean }) => void; + preboot.holdSetupUntilResolved( + 'some-other-plugin', + 'some-other-reason', + new Promise<{ shouldReloadConfig: boolean } | undefined>((resolve) => { + resolveSecondPromise = resolve; + }) + ); + + expect(preboot.isSetupOnHold()).toBe(true); + const waitUntilPromise = preboot.waitUntilCanSetup(); + + resolveFirstPromise!(); + await nextTick(); + expect(preboot.isSetupOnHold()).toBe(true); + + resolveSecondPromise!(); + await nextTick(); + expect(preboot.isSetupOnHold()).toBe(false); + + await expect(waitUntilPromise).resolves.toEqual({ shouldReloadConfig: false }); + }); + + it('#holdSetupUntilResolved logs a reason', async () => { + const preboot = service.preboot(); + + preboot.holdSetupUntilResolved( + 'some-plugin', + 'some-reason', + Promise.resolve({ shouldReloadConfig: true }) + ); + preboot.holdSetupUntilResolved( + 'some-other-plugin', + 'some-other-reason', + Promise.resolve(undefined) + ); + + expect(loggingSystemMock.collect(logger).info).toMatchInlineSnapshot(` + Array [ + Array [ + "\\"some-plugin\\" plugin is holding setup: some-reason", + ], + Array [ + "\\"some-other-plugin\\" plugin is holding setup: some-other-reason", + ], + ] + `); + + await expect(preboot.waitUntilCanSetup()).resolves.toEqual({ shouldReloadConfig: true }); + }); + + it('#holdSetupUntilResolved does not allow to register new promises after #waitUntilCanSetup is called', async () => { + const preboot = service.preboot(); + + preboot.holdSetupUntilResolved( + 'some-plugin', + 'some-reason', + Promise.resolve({ shouldReloadConfig: true }) + ); + + const waitUntilPromise = preboot.waitUntilCanSetup(); + + expect(() => + preboot.holdSetupUntilResolved( + 'some-other-plugin', + 'some-other-reason', + Promise.resolve(undefined) + ) + ).toThrowErrorMatchingInlineSnapshot(`"Cannot hold boot at this stage."`); + + expect(loggingSystemMock.collect(logger).info).toMatchInlineSnapshot(` + Array [ + Array [ + "\\"some-plugin\\" plugin is holding setup: some-reason", + ], + ] + `); + + await expect(waitUntilPromise).resolves.toEqual({ shouldReloadConfig: true }); + }); + + it('#waitUntilCanSetup returns `shouldReloadConfig` set to `true` if at least one promise did it', async () => { + const preboot = service.preboot(); + + expect(preboot.isSetupOnHold()).toBe(false); + + let resolveFirstPromise: (value?: { shouldReloadConfig: boolean }) => void; + preboot.holdSetupUntilResolved( + 'some-plugin', + 'some-reason', + new Promise<{ shouldReloadConfig: boolean } | undefined>((resolve) => { + resolveFirstPromise = resolve; + }) + ); + + let resolveSecondPromise: (value?: { shouldReloadConfig: boolean }) => void; + preboot.holdSetupUntilResolved( + 'some-other-plugin', + 'some-other-reason', + new Promise<{ shouldReloadConfig: boolean } | undefined>((resolve) => { + resolveSecondPromise = resolve; + }) + ); + + expect(preboot.isSetupOnHold()).toBe(true); + const waitUntilPromise = preboot.waitUntilCanSetup(); + + resolveFirstPromise!({ shouldReloadConfig: true }); + await nextTick(); + expect(preboot.isSetupOnHold()).toBe(true); + + resolveSecondPromise!({ shouldReloadConfig: false }); + await nextTick(); + expect(preboot.isSetupOnHold()).toBe(false); + + await expect(waitUntilPromise).resolves.toEqual({ shouldReloadConfig: true }); + }); + + it('#waitUntilCanSetup is rejected if at least one promise is rejected', async () => { + const preboot = service.preboot(); + + preboot.holdSetupUntilResolved( + 'some-plugin', + 'some-reason', + Promise.resolve({ shouldReloadConfig: true }) + ); + preboot.holdSetupUntilResolved( + 'some-other-plugin', + 'some-other-reason', + Promise.reject('Uh oh!') + ); + + await expect(preboot.waitUntilCanSetup()).rejects.toBe('Uh oh!'); + }); + }); +}); diff --git a/src/core/server/preboot/preboot_service.ts b/src/core/server/preboot/preboot_service.ts new file mode 100644 index 0000000000000..4313541ef91d3 --- /dev/null +++ b/src/core/server/preboot/preboot_service.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreContext } from '../core_context'; +import { InternalPrebootServicePreboot } from './types'; + +/** @internal */ +export class PrebootService { + private readonly promiseList: Array> = []; + private waitUntilCanSetupPromise?: Promise<{ shouldReloadConfig: boolean }>; + private isSetupOnHold = false; + private readonly log = this.core.logger.get('preboot'); + + constructor(private readonly core: CoreContext) {} + + public preboot(): InternalPrebootServicePreboot { + return { + isSetupOnHold: () => this.isSetupOnHold, + holdSetupUntilResolved: (pluginName, reason, promise) => { + if (this.waitUntilCanSetupPromise) { + throw new Error('Cannot hold boot at this stage.'); + } + + this.log.info(`"${pluginName}" plugin is holding setup: ${reason}`); + + this.isSetupOnHold = true; + + this.promiseList.push(promise); + }, + waitUntilCanSetup: () => { + if (!this.waitUntilCanSetupPromise) { + this.waitUntilCanSetupPromise = Promise.all(this.promiseList) + .then((results) => ({ + shouldReloadConfig: results.some((result) => result?.shouldReloadConfig), + })) + .catch((err) => { + this.log.error(err); + throw err; + }) + .finally(() => (this.isSetupOnHold = false)); + } + + return this.waitUntilCanSetupPromise; + }, + }; + } + + public stop() { + this.isSetupOnHold = false; + this.promiseList.length = 0; + this.waitUntilCanSetupPromise = undefined; + } +} diff --git a/src/core/server/preboot/types.ts b/src/core/server/preboot/types.ts new file mode 100644 index 0000000000000..61abc327c9ddb --- /dev/null +++ b/src/core/server/preboot/types.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginName } from '..'; + +/** @internal */ +export interface InternalPrebootServicePreboot { + /** + * Indicates whether Kibana is currently on hold and cannot proceed to `setup` yet. + */ + readonly isSetupOnHold: () => boolean; + + /** + * Registers a `Promise` as a precondition before Kibana can proceed to `setup`. This method can be invoked multiple + * times and from multiple `preboot` plugins. Kibana will proceed to `setup` only when all registered `Promise` are + * resolved, or it will shut down if any of them are rejected. + * @param pluginName Name of the plugin that needs to hold `setup`. + * @param reason A string that explains the reason why this promise should hold `setup`. It's supposed to be a human + * readable string that will be recorded in the logs or standard output. + * @param promise A `Promise` that should resolved before Kibana can proceed to `setup`. + */ + readonly holdSetupUntilResolved: ( + pluginName: PluginName, + reason: string, + promise: Promise<{ shouldReloadConfig: boolean } | undefined> + ) => void; + + /** + * Returns a `Promise` that is resolved only when all `Promise` instances registered with {@link holdSetupUntilResolved} + * are resolved, or rejected if any of them are rejected. If the supplied `Promise` resolves to an object with the + * `shouldReloadConfig` property set to `true`, it indicates that Kibana configuration might have changed and Kibana + * needs to reload it from the disk. + */ + readonly waitUntilCanSetup: () => Promise<{ shouldReloadConfig: boolean }>; +} + +/** + * Kibana Preboot Service allows to control the boot flow of Kibana. Preboot plugins can use it to hold the boot until certain condition is met. + * + * @example + * A plugin can supply a `Promise` to a `holdSetupUntilResolved` method to signal Kibana to initialize and start `standard` plugins only after this + * `Promise` is resolved. If `Promise` is rejected, Kibana will shut down. + * ```ts + * core.preboot.holdSetupUntilResolved('Just waiting for 5 seconds', + * new Promise((resolve) => { + * setTimeout(resolve, 5000); + * }) + * ); + * ``` + * + * If the supplied `Promise` resolves to an object with the `shouldReloadConfig` property set to `true`, Kibana will also reload its configuration from disk. + * ```ts + * let completeSetup: (result: { shouldReloadConfig: boolean }) => void; + * core.preboot.holdSetupUntilResolved('Just waiting for 5 seconds before reloading configuration', + * new Promise<{ shouldReloadConfig: boolean }>((resolve) => { + * setTimeout(() => resolve({ shouldReloadConfig: true }), 5000); + * }) + * ); + * ``` + * @public + */ +export interface PrebootServicePreboot { + /** + * Indicates whether Kibana is currently on hold and cannot proceed to `setup` yet. + */ + readonly isSetupOnHold: () => boolean; + + /** + * Registers a `Promise` as a precondition before Kibana can proceed to `setup`. This method can be invoked multiple + * times and from multiple `preboot` plugins. Kibana will proceed to `setup` only when all registered `Promises` + * instances are resolved, or it will shut down if any of them is rejected. + * @param reason A string that explains the reason why this promise should hold `setup`. It's supposed to be a human + * readable string that will be recorded in the logs or standard output. + * @param promise A `Promise` that should resolved before Kibana can proceed to `setup`. + */ + readonly holdSetupUntilResolved: ( + reason: string, + promise: Promise<{ shouldReloadConfig: boolean } | undefined> + ) => void; +} diff --git a/src/core/server/preboot_core_route_handler_context.test.ts b/src/core/server/preboot_core_route_handler_context.test.ts new file mode 100644 index 0000000000000..8d090d8644637 --- /dev/null +++ b/src/core/server/preboot_core_route_handler_context.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PrebootCoreRouteHandlerContext } from './preboot_core_route_handler_context'; +import { coreMock } from './mocks'; + +describe('#uiSettings', () => { + describe('#client', () => { + test('returns the results of corePreboot.uiSettings.createDefaultsClient', () => { + const corePreboot = coreMock.createInternalPreboot(); + const context = new PrebootCoreRouteHandlerContext(corePreboot); + + const client = context.uiSettings.client; + const [{ value: mockResult }] = corePreboot.uiSettings.createDefaultsClient.mock.results; + expect(client).toBe(mockResult); + }); + + test('only creates one instance', () => { + const corePreboot = coreMock.createInternalPreboot(); + const context = new PrebootCoreRouteHandlerContext(corePreboot); + + const client1 = context.uiSettings.client; + const client2 = context.uiSettings.client; + + expect(corePreboot.uiSettings.createDefaultsClient).toHaveBeenCalledTimes(1); + const [{ value: mockResult }] = corePreboot.uiSettings.createDefaultsClient.mock.results; + expect(client1).toBe(mockResult); + expect(client2).toBe(mockResult); + }); + }); +}); diff --git a/src/core/server/preboot_core_route_handler_context.ts b/src/core/server/preboot_core_route_handler_context.ts new file mode 100644 index 0000000000000..63378046e8050 --- /dev/null +++ b/src/core/server/preboot_core_route_handler_context.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// eslint-disable-next-line max-classes-per-file +import { InternalCorePreboot } from './internal_types'; +import { IUiSettingsClient } from './ui_settings'; + +class PrebootCoreUiSettingsRouteHandlerContext { + constructor(public readonly client: IUiSettingsClient) {} +} + +export class PrebootCoreRouteHandlerContext { + readonly uiSettings: PrebootCoreUiSettingsRouteHandlerContext; + + constructor(private readonly corePreboot: InternalCorePreboot) { + this.uiSettings = new PrebootCoreUiSettingsRouteHandlerContext( + this.corePreboot.uiSettings.createDefaultsClient() + ); + } +} diff --git a/src/core/server/rendering/__mocks__/params.ts b/src/core/server/rendering/__mocks__/params.ts index d790e4a679988..091d185cceefc 100644 --- a/src/core/server/rendering/__mocks__/params.ts +++ b/src/core/server/rendering/__mocks__/params.ts @@ -12,13 +12,17 @@ import { pluginServiceMock } from '../../plugins/plugins_service.mock'; import { statusServiceMock } from '../../status/status_service.mock'; const context = mockCoreContext.create(); -const http = httpServiceMock.createInternalSetupContract(); -const uiPlugins = pluginServiceMock.createUiPlugins(); +const httpPreboot = httpServiceMock.createInternalPrebootContract(); +const httpSetup = httpServiceMock.createInternalSetupContract(); const status = statusServiceMock.createInternalSetupContract(); export const mockRenderingServiceParams = context; +export const mockRenderingPrebootDeps = { + http: httpPreboot, + uiPlugins: pluginServiceMock.createUiPlugins(), +}; export const mockRenderingSetupDeps = { - http, - uiPlugins, + http: httpSetup, + uiPlugins: pluginServiceMock.createUiPlugins(), status, }; diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts index 56131d77b23eb..76558a999a374 100644 --- a/src/core/server/rendering/__mocks__/rendering_service.ts +++ b/src/core/server/rendering/__mocks__/rendering_service.ts @@ -8,17 +8,22 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import type { RenderingService as Service } from '../rendering_service'; -import type { InternalRenderingServiceSetup } from '../types'; +import type { InternalRenderingServicePreboot, InternalRenderingServiceSetup } from '../types'; import { mockRenderingServiceParams } from './params'; type IRenderingService = PublicMethodsOf; +export const prebootMock: jest.Mocked = { + render: jest.fn(), +}; export const setupMock: jest.Mocked = { render: jest.fn(), }; +export const mockPreboot = jest.fn().mockResolvedValue(prebootMock); export const mockSetup = jest.fn().mockResolvedValue(setupMock); export const mockStop = jest.fn(); export const mockRenderingService: jest.Mocked = { + preboot: mockPreboot, setup: mockSetup, stop: mockStop, }; diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index f6b39ea24262b..495a38a1af5bf 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -1,5 +1,264 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`RenderingService preboot() render() renders "core" from legacy request 1`] = ` +Object { + "anonymousStatusPage": false, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "mode": Object { + "dev": Any, + "name": Any, + "prod": Any, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": Any, + "version": Any, + }, + }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", + "serverBasePath": "/mock-server-basepath", + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService preboot() render() renders "core" page 1`] = ` +Object { + "anonymousStatusPage": false, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "mode": Object { + "dev": Any, + "name": Any, + "prod": Any, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": Any, + "version": Any, + }, + }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", + "serverBasePath": "/mock-server-basepath", + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService preboot() render() renders "core" page driven by settings 1`] = ` +Object { + "anonymousStatusPage": false, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "mode": Object { + "dev": Any, + "name": Any, + "prod": Any, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": Any, + "version": Any, + }, + }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object { + "theme:darkMode": Object { + "userValue": true, + }, + }, + }, + }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", + "serverBasePath": "/mock-server-basepath", + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService preboot() render() renders "core" page for blank basepath 1`] = ` +Object { + "anonymousStatusPage": false, + "basePath": "", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "mode": Object { + "dev": Any, + "name": Any, + "prod": Any, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": Any, + "version": Any, + }, + }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, + "i18n": Object { + "translationsUrl": "/translations/en.json", + }, + "legacyMetadata": Object { + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", + "serverBasePath": "/mock-server-basepath", + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService preboot() render() renders "core" with excluded user settings 1`] = ` +Object { + "anonymousStatusPage": false, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "mode": Object { + "dev": Any, + "name": Any, + "prod": Any, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": Any, + "version": Any, + }, + }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", + "serverBasePath": "/mock-server-basepath", + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + exports[`RenderingService setup() render() renders "core" from legacy request 1`] = ` Object { "anonymousStatusPage": false, diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts index 0abd8fd5a0057..67f8507b9b700 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { InternalPluginInfo, UiPlugins } from '../../plugins'; +import { InternalPluginInfo, PluginType, UiPlugins } from '../../plugins'; import { getPluginsBundlePaths } from './get_plugin_bundle_paths'; const createUiPlugins = (pluginDeps: Record) => { @@ -26,6 +26,7 @@ const createUiPlugins = (pluginDeps: Record) => { uiPlugins.public.set(pluginId, { id: pluginId, configPath: 'config-path', + type: PluginType.standard, optionalPlugins: [], requiredPlugins: [], requiredBundles: deps, diff --git a/src/core/server/rendering/rendering_service.mock.ts b/src/core/server/rendering/rendering_service.mock.ts index 81418c58175f5..3d8213da62c6d 100644 --- a/src/core/server/rendering/rendering_service.mock.ts +++ b/src/core/server/rendering/rendering_service.mock.ts @@ -6,7 +6,14 @@ * Side Public License, v 1. */ -import { InternalRenderingServiceSetup } from './types'; +import { InternalRenderingServicePreboot, InternalRenderingServiceSetup } from './types'; + +function createRenderingPreboot() { + const mocked: jest.Mocked = { + render: jest.fn().mockResolvedValue(''), + }; + return mocked; +} function createRenderingSetup() { const mocked: jest.Mocked = { @@ -16,5 +23,6 @@ function createRenderingSetup() { } export const renderingMock = { + createPrebootContract: createRenderingPreboot, createSetupContract: createRenderingSetup, }; diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index bba0dc6fd8a67..de7d21add6c6c 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -16,9 +16,14 @@ import { import { load } from 'cheerio'; import { httpServerMock } from '../http/http_server.mocks'; +import { mockRouter } from '../http/router/router.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; -import { mockRenderingServiceParams, mockRenderingSetupDeps } from './__mocks__/params'; -import { InternalRenderingServiceSetup } from './types'; +import { + mockRenderingServiceParams, + mockRenderingPrebootDeps, + mockRenderingSetupDeps, +} from './__mocks__/params'; +import { InternalRenderingServicePreboot, InternalRenderingServiceSetup } from './types'; import { RenderingService } from './rendering_service'; const INJECTED_METADATA = { @@ -43,106 +48,145 @@ const INJECTED_METADATA = { const { createKibanaRequest, createRawRequest } = httpServerMock; -describe('RenderingService', () => { - let service: RenderingService; +function renderTestCases( + getRender: () => Promise< + [ + InternalRenderingServicePreboot['render'] | InternalRenderingServiceSetup['render'], + typeof mockRenderingPrebootDeps | typeof mockRenderingSetupDeps + ] + > +) { + describe('render()', () => { + let uiSettings: ReturnType; + + beforeEach(async () => { + uiSettings = uiSettingsServiceMock.createClient(); + uiSettings.getRegistered.mockReturnValue({ + registered: { name: 'title' }, + }); + }); - beforeEach(() => { - jest.clearAllMocks(); - service = new RenderingService(mockRenderingServiceParams); + it('renders "core" page', async () => { + const [render] = await getRender(); + const content = await render(createKibanaRequest(), uiSettings); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); - getSettingValueMock.mockImplementation((settingName: string) => settingName); - getStylesheetPathsMock.mockReturnValue(['/style-1.css', '/style-2.css']); - }); + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); - describe('setup()', () => { - it('calls `registerBootstrapRoute` with the correct parameters', async () => { - await service.setup(mockRenderingSetupDeps); + it('renders "core" page for blank basepath', async () => { + const [render, deps] = await getRender(); + deps.http.basePath.get.mockReturnValueOnce(''); - expect(registerBootstrapRouteMock).toHaveBeenCalledTimes(1); - expect(registerBootstrapRouteMock).toHaveBeenCalledWith({ - router: expect.any(Object), - renderer: bootstrapRendererMock, - }); - }); + const content = await render(createKibanaRequest(), uiSettings); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); - describe('render()', () => { - let uiSettings: ReturnType; - let render: InternalRenderingServiceSetup['render']; + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); - beforeEach(async () => { - uiSettings = uiSettingsServiceMock.createClient(); - uiSettings.getRegistered.mockReturnValue({ - registered: { name: 'title' }, - }); - render = (await service.setup(mockRenderingSetupDeps)).render; - }); + it('renders "core" page driven by settings', async () => { + uiSettings.getUserProvided.mockResolvedValue({ 'theme:darkMode': { userValue: true } }); + const [render] = await getRender(); + const content = await render(createKibanaRequest(), uiSettings); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); - it('renders "core" page', async () => { - const content = await render(createKibanaRequest(), uiSettings); - const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); - expect(data).toMatchSnapshot(INJECTED_METADATA); + it('renders "core" with excluded user settings', async () => { + const [render] = await getRender(); + const content = await render(createKibanaRequest(), uiSettings, { + includeUserSettings: false, }); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); - it('renders "core" page for blank basepath', async () => { - mockRenderingSetupDeps.http.basePath.get.mockReturnValueOnce(''); + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); - const content = await render(createKibanaRequest(), uiSettings); - const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); + it('renders "core" from legacy request', async () => { + const [render] = await getRender(); + const content = await render(createRawRequest(), uiSettings); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); - expect(data).toMatchSnapshot(INJECTED_METADATA); + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('calls `getStylesheetPaths` with the correct parameters', async () => { + getSettingValueMock.mockImplementation((settingName: string) => { + if (settingName === 'theme:darkMode') { + return true; + } + if (settingName === 'theme:version') { + return 'v8'; + } + return settingName; }); - it('renders "core" page driven by settings', async () => { - uiSettings.getUserProvided.mockResolvedValue({ 'theme:darkMode': { userValue: true } }); - const content = await render(createKibanaRequest(), uiSettings); - const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); + const [render] = await getRender(); + await render(createKibanaRequest(), uiSettings); - expect(data).toMatchSnapshot(INJECTED_METADATA); + expect(getStylesheetPathsMock).toHaveBeenCalledTimes(1); + expect(getStylesheetPathsMock).toHaveBeenCalledWith({ + darkMode: true, + themeVersion: 'v8', + basePath: '/mock-server-basepath', + buildNum: expect.any(Number), }); + }); + }); +} - it('renders "core" with excluded user settings', async () => { - const content = await render(createKibanaRequest(), uiSettings, { - includeUserSettings: false, - }); - const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); +describe('RenderingService', () => { + let service: RenderingService; - expect(data).toMatchSnapshot(INJECTED_METADATA); - }); + beforeEach(() => { + jest.clearAllMocks(); + service = new RenderingService(mockRenderingServiceParams); - it('renders "core" from legacy request', async () => { - const content = await render(createRawRequest(), uiSettings); - const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); + getSettingValueMock.mockImplementation((settingName: string) => settingName); + getStylesheetPathsMock.mockReturnValue(['/style-1.css', '/style-2.css']); + }); + + describe('preboot()', () => { + it('calls `registerBootstrapRoute` with the correct parameters', async () => { + const routerMock = mockRouter.create(); + mockRenderingPrebootDeps.http.registerRoutes.mockImplementation((path, callback) => + callback(routerMock) + ); + + await service.preboot(mockRenderingPrebootDeps); - expect(data).toMatchSnapshot(INJECTED_METADATA); + expect(registerBootstrapRouteMock).toHaveBeenCalledTimes(1); + expect(registerBootstrapRouteMock).toHaveBeenCalledWith({ + router: routerMock, + renderer: bootstrapRendererMock, }); + }); + + renderTestCases(async () => { + return [(await service.preboot(mockRenderingPrebootDeps)).render, mockRenderingPrebootDeps]; + }); + }); + + describe('setup()', () => { + it('calls `registerBootstrapRoute` with the correct parameters', async () => { + await service.setup(mockRenderingSetupDeps); - it('calls `getStylesheetPaths` with the correct parameters', async () => { - getSettingValueMock.mockImplementation((settingName: string) => { - if (settingName === 'theme:darkMode') { - return true; - } - if (settingName === 'theme:version') { - return 'v8'; - } - return settingName; - }); - - await render(createKibanaRequest(), uiSettings); - - expect(getStylesheetPathsMock).toHaveBeenCalledTimes(1); - expect(getStylesheetPathsMock).toHaveBeenCalledWith({ - darkMode: true, - themeVersion: 'v8', - basePath: '/mock-server-basepath', - buildNum: expect.any(Number), - }); + expect(registerBootstrapRouteMock).toHaveBeenCalledTimes(1); + expect(registerBootstrapRouteMock).toHaveBeenCalledWith({ + router: expect.any(Object), + renderer: bootstrapRendererMock, }); }); + + renderTestCases(async () => { + await service.preboot(mockRenderingPrebootDeps); + return [(await service.setup(mockRenderingSetupDeps)).render, mockRenderingSetupDeps]; + }); }); }); diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index fd4e1140d68b4..2d95822d92219 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -16,100 +16,130 @@ import { CoreContext } from '../core_context'; import { Template } from './views'; import { IRenderOptions, + RenderingPrebootDeps, RenderingSetupDeps, + InternalRenderingServicePreboot, InternalRenderingServiceSetup, RenderingMetadata, } from './types'; import { registerBootstrapRoute, bootstrapRendererFactory } from './bootstrap'; import { getSettingValue, getStylesheetPaths } from './render_utils'; +import { KibanaRequest, LegacyRequest } from '../http'; +import { IUiSettingsClient } from '../ui_settings'; + +type RenderOptions = (RenderingPrebootDeps & { status?: never }) | RenderingSetupDeps; /** @internal */ export class RenderingService { constructor(private readonly coreContext: CoreContext) {} + public async preboot({ + http, + uiPlugins, + }: RenderingPrebootDeps): Promise { + http.registerRoutes('', (router) => { + registerBootstrapRoute({ + router, + renderer: bootstrapRendererFactory({ + uiPlugins, + serverBasePath: http.basePath.serverBasePath, + packageInfo: this.coreContext.env.packageInfo, + auth: http.auth, + }), + }); + }); + + return { + render: this.render.bind(this, { http, uiPlugins }), + }; + } + public async setup({ http, status, uiPlugins, }: RenderingSetupDeps): Promise { - const router = http.createRouter(''); - - const bootstrapRenderer = bootstrapRendererFactory({ - uiPlugins, - serverBasePath: http.basePath.serverBasePath, - packageInfo: this.coreContext.env.packageInfo, - auth: http.auth, + registerBootstrapRoute({ + router: http.createRouter(''), + renderer: bootstrapRendererFactory({ + uiPlugins, + serverBasePath: http.basePath.serverBasePath, + packageInfo: this.coreContext.env.packageInfo, + auth: http.auth, + }), }); - registerBootstrapRoute({ router, renderer: bootstrapRenderer }); return { - render: async ( - request, - uiSettings, - { includeUserSettings = true, vars }: IRenderOptions = {} - ) => { - const env = { - mode: this.coreContext.env.mode, - packageInfo: this.coreContext.env.packageInfo, - }; - const buildNum = env.packageInfo.buildNum; - const basePath = http.basePath.get(request); - const { serverBasePath, publicBaseUrl } = http.basePath; - const settings = { - defaults: uiSettings.getRegistered(), - user: includeUserSettings ? await uiSettings.getUserProvided() : {}, - }; + render: this.render.bind(this, { http, uiPlugins, status }), + }; + } - const darkMode = getSettingValue('theme:darkMode', settings, Boolean); - const themeVersion = getSettingValue('theme:version', settings, String); + private async render( + { http, uiPlugins, status }: RenderOptions, + request: KibanaRequest | LegacyRequest, + uiSettings: IUiSettingsClient, + { includeUserSettings = true, vars }: IRenderOptions = {} + ) { + const env = { + mode: this.coreContext.env.mode, + packageInfo: this.coreContext.env.packageInfo, + }; + const buildNum = env.packageInfo.buildNum; + const basePath = http.basePath.get(request); + const { serverBasePath, publicBaseUrl } = http.basePath; + const settings = { + defaults: uiSettings.getRegistered() ?? {}, + user: includeUserSettings ? await uiSettings.getUserProvided() : {}, + }; - const stylesheetPaths = getStylesheetPaths({ - darkMode, - themeVersion, - basePath: serverBasePath, - buildNum, - }); + const darkMode = getSettingValue('theme:darkMode', settings, Boolean); + const themeVersion = getSettingValue('theme:version', settings, String); - const metadata: RenderingMetadata = { - strictCsp: http.csp.strict, - uiPublicUrl: `${basePath}/ui`, - bootstrapScriptUrl: `${basePath}/bootstrap.js`, - i18n: i18n.translate, - locale: i18n.getLocale(), - darkMode, - stylesheetPaths, - themeVersion, - injectedMetadata: { - version: env.packageInfo.version, - buildNumber: env.packageInfo.buildNum, - branch: env.packageInfo.branch, - basePath, - serverBasePath, - publicBaseUrl, - env, - anonymousStatusPage: status.isStatusPageAnonymous(), - i18n: { - translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, - }, - csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers }, - externalUrl: http.externalUrl, - vars: vars ?? {}, - uiPlugins: await Promise.all( - [...uiPlugins.public].map(async ([id, plugin]) => ({ - id, - plugin, - config: await getUiConfig(uiPlugins, id), - })) - ), - legacyMetadata: { - uiSettings: settings, - }, - }, - }; + const stylesheetPaths = getStylesheetPaths({ + darkMode, + themeVersion, + basePath: serverBasePath, + buildNum, + }); - return `${renderToStaticMarkup(