diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 168db2c4efb84c..0744112650c23f 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 d9a8d0558714f0..fe4c8a9280158b 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 9b8a9c64ac43b5..a8eb619a8eab83 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 53ffee5e061d60..f4d35a2d554ba3 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 f96c8746ef17ad..28a2dc0013f600 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 a867477938219a..c5080da40d97ee 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 ea3b24d80b8c31..6b9ff7c8f9d9fb 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 93973b8de0a54a..a139cab486257c 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 5db819ce648e83..eb0c0f984f4839 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 e3a1ce2216c1c6..7bf7839ec773da 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 b77d55df2d9ea8..462687de720e0f 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 43e5ae733a7604..ae6e48556f684e 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 a92fc182f388c3..53d445b932d627 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 fa626f6eaa913e..61113b5491c297 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/dev-tools/console/console.asciidoc b/docs/dev-tools/console/console.asciidoc index 7cef9376330623..731290fea22d0c 100644 --- a/docs/dev-tools/console/console.asciidoc +++ b/docs/dev-tools/console/console.asciidoc @@ -1,7 +1,7 @@ [[console-kibana]] == Console -Console enables you to interact with the REST API of {es}. You can: +*Console* enables you to interact with the REST API of {es}. You can: * Send requests to {es} and view the responses * View API documentation @@ -12,13 +12,13 @@ To get started, open the main menu, click *Dev Tools*, then click *Console*. [role="screenshot"] image::dev-tools/console/images/console.png["Console"] -NOTE: You are unable to interact with the REST API of {kib} with the Console. +NOTE: You cannot to interact with the REST API of {kib} with the Console. [float] [[console-api]] === Write requests -Console understands commands in a cURL-like syntax. +*Console* understands commands in a cURL-like syntax. For example, the following is a `GET` request to the {es} `_search` API. [source,js] @@ -43,8 +43,8 @@ curl -XGET "http://localhost:9200/_search" -d' }' ---------------------------------- -When you paste the command into Console, {kib} automatically converts it -to Console syntax. Alternatively, if you want to see Console syntax in cURL, +When you paste the command into *Console*, {kib} automatically converts it +to *Console* syntax. Alternatively, if you want to see *Console* syntax in cURL, click the action icon (image:dev-tools/console/images/wrench.png[]) and select *Copy as cURL*. Once copied, the username and password will need to be provided for the calls to work from external environments. @@ -53,7 +53,7 @@ for the calls to work from external environments. [[console-autocomplete]] ==== Autocomplete -When you're typing a command, Console makes context-sensitive suggestions. +When you're typing a command, *Console* makes context-sensitive suggestions. These suggestions show you the parameters for each API and speed up your typing. To configure your preferences for autocomplete, go to <>. @@ -69,15 +69,16 @@ and then select *Auto indent*. For example, you might have a request formatted like this: [role="screenshot"] -image::dev-tools/console/images/copy-curl.png["Console close-up"] +image::dev-tools/console/images/copy-curl.png["Console close-up", width=75%] +] -Console adjusts the JSON body of the request to apply the indents. +*Console* adjusts the JSON body of the request to apply the indents. [role="screenshot"] -image::dev-tools/console/images/request.png["Console close-up"] +image::dev-tools/console/images/request.png["Console close-up", width=75%] If you select *Auto indent* on a request that is already well formatted, -Console collapses the request body to a single line per document. +*Console* collapses the request body to a single line per document. This is helpful when working with the {es} {ref}/docs-bulk.html[bulk APIs]. @@ -90,8 +91,9 @@ When you're ready to submit the request to {es}, click the green triangle. You can select multiple requests and submit them together. -Console sends the requests to {es} one by one and shows the output -in the response pane. Submitting multiple request is helpful when you're debugging an issue or trying query +*Console* sends the requests to {es} one by one and shows the output +in the response pane. Submitting multiple requests is helpful +when you're debugging an issue or trying query combinations in multiple scenarios. @@ -107,7 +109,7 @@ the action icon (image:dev-tools/console/images/wrench.png[]) and select [[console-history]] === Get your request history -Console maintains a list of the last 500 requests that {es} successfully executed. +*Console* maintains a list of the last 500 requests that {es} successfully executed. To view your most recent requests, click *History*. If you select a request and click *Apply*, {kib} adds it to the editor at the current cursor position. @@ -115,11 +117,11 @@ and click *Apply*, {kib} adds it to the editor at the current cursor position. [[configuring-console]] === Configure Console settings -You can configure the Console font size, JSON syntax, +You can configure the *Console* font size, JSON syntax, and autocomplete suggestions in *Settings*. [role="screenshot"] -image::dev-tools/console/images/console-settings.png["Console Settings"] +image::dev-tools/console/images/console-settings.png["Console Settings", width=60%] [float] [[keyboard-shortcuts]] @@ -132,7 +134,7 @@ shortcuts, click *Help*. [[console-settings]] === Disable Console -If you don’t want to use Console, you can disable it by setting `console.enabled` +If you don’t want to use *Console*, you can disable it by setting `console.enabled` to `false` in your `kibana.yml` configuration file. Changing this setting causes the server to regenerate assets on the next startup, which might cause a delay before pages start being served. diff --git a/docs/dev-tools/console/images/console.png b/docs/dev-tools/console/images/console.png index 0511ed858d1c3a..88f069388ea676 100644 Binary files a/docs/dev-tools/console/images/console.png and b/docs/dev-tools/console/images/console.png differ diff --git a/docs/dev-tools/console/images/copy-curl.png b/docs/dev-tools/console/images/copy-curl.png index 4811c1d24bfb86..a6fb9cd1438f47 100644 Binary files a/docs/dev-tools/console/images/copy-curl.png and b/docs/dev-tools/console/images/copy-curl.png differ diff --git a/docs/dev-tools/console/images/request.png b/docs/dev-tools/console/images/request.png index a8332434ec1865..c95b54dc95b0ac 100644 Binary files a/docs/dev-tools/console/images/request.png and b/docs/dev-tools/console/images/request.png differ diff --git a/docs/dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png b/docs/dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png index 2cb6f1dbf7226f..2a1660c860b4b4 100644 Binary files a/docs/dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png and b/docs/dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png differ diff --git a/docs/dev-tools/grokdebugger/images/grok-debugger-overview.png b/docs/dev-tools/grokdebugger/images/grok-debugger-overview.png index b6e9b734b307e3..4692c7a8020673 100644 Binary files a/docs/dev-tools/grokdebugger/images/grok-debugger-overview.png and b/docs/dev-tools/grokdebugger/images/grok-debugger-overview.png differ diff --git a/docs/dev-tools/grokdebugger/index.asciidoc b/docs/dev-tools/grokdebugger/index.asciidoc index 82ae724f705f65..934452c54ccca6 100644 --- a/docs/dev-tools/grokdebugger/index.asciidoc +++ b/docs/dev-tools/grokdebugger/index.asciidoc @@ -1,19 +1,19 @@ [role="xpack"] [[xpack-grokdebugger]] -== Debugging grok expressions +== Debug grok expressions You can build and debug grok patterns in the {kib} *Grok Debugger* -before you use them in your data processing pipelines. Grok is a pattern +before you use them in your data processing pipelines. Grok is a pattern matching syntax that you can use to parse arbitrary text and structure it. Grok is good for parsing syslog, apache, and other webserver logs, mysql logs, and in general, any log format that is -written for human consumption. +written for human consumption. Grok patterns are supported in the ingest node {ref}/grok-processor.html[grok processor] and the Logstash -{logstash-ref}/plugins-filters-grok.html[grok filter]. See +{logstash-ref}/plugins-filters-grok.html[grok filter]. See {logstash-ref}/plugins-filters-grok.html#_grok_basics[grok basics] -for more information on the syntax for a grok pattern. +for more information on the syntax for a grok pattern. The Elastic Stack ships with more than 120 reusable grok patterns. See @@ -27,10 +27,10 @@ in ingest node and Logstash. [float] [[grokdebugger-getting-started]] -=== Getting started with the Grok Debugger +=== Get started This example walks you through using the *Grok Debugger*. This tool -is automatically enabled in {kib}. +is automatically enabled in {kib}. NOTE: If you're using {stack-security-features}, you must have the `manage_pipeline` permission to use the Grok Debugger. @@ -66,12 +66,12 @@ image::dev-tools/grokdebugger/images/grok-debugger-overview.png["Grok Debugger"] [float] [[grokdebugger-custom-patterns]] -=== Testing custom patterns +=== Test custom patterns If the default grok pattern dictionary doesn't contain the patterns you need, -you can define, test, and debug custom patterns using the Grok Debugger. +you can define, test, and debug custom patterns using the *Grok Debugger*. -Custom patterns that you enter in the Grok Debugger are not saved. Custom patterns +Custom patterns that you enter in the *Grok Debugger* are not saved. Custom patterns are only available for the current debugging session and have no side effects. Follow this example to define a custom pattern. diff --git a/docs/dev-tools/painlesslab/images/painless-lab.png b/docs/dev-tools/painlesslab/images/painless-lab.png index fbfd54f69954dd..65b4141ed5c547 100644 Binary files a/docs/dev-tools/painlesslab/images/painless-lab.png and b/docs/dev-tools/painlesslab/images/painless-lab.png differ diff --git a/docs/dev-tools/painlesslab/index.asciidoc b/docs/dev-tools/painlesslab/index.asciidoc index 5e329843843ec1..7b4e9101a99013 100644 --- a/docs/dev-tools/painlesslab/index.asciidoc +++ b/docs/dev-tools/painlesslab/index.asciidoc @@ -4,7 +4,7 @@ beta::[] -The Painless Lab is an interactive code editor that lets you test and +The *Painless Lab* is an interactive code editor that lets you test and debug {ref}/modules-scripting-painless.html[Painless scripts] in real-time. You can use the Painless scripting language to create <>, @@ -12,6 +12,7 @@ process {ref}/docs-reindex.html[reindexed data], define complex <>, and work with data in other contexts. -To get started, open the main menu, click *Dev Tools*, then click *Painless Lab*. +To get started, open the main menu, click *Dev Tools*, and then click *Painless Lab*. +[role="screenshot"] image::dev-tools/painlesslab/images/painless-lab.png[Painless Lab] diff --git a/docs/dev-tools/searchprofiler/getting-started.asciidoc b/docs/dev-tools/searchprofiler/getting-started.asciidoc deleted file mode 100644 index ad73d03bcbfd8a..00000000000000 --- a/docs/dev-tools/searchprofiler/getting-started.asciidoc +++ /dev/null @@ -1,49 +0,0 @@ -[role="xpack"] -[[profiler-getting-started]] -=== Getting Started - -The {searchprofiler} is automatically enabled in {kib}. Open the main menu, click *Dev Tools*, then click *{searchprofiler}* -to get started. - -{searchprofiler} displays the names of the indices searched, the shards in each index, -and how long it took for the query to complete. To try it out, replace the default `match_all` query -with the query you want to profile and click *Profile*. - -The following example shows the results of profiling the `match_all` query. -If we take a closer look at the information for the `.kibana_1` sample index, the -Cumulative Time field shows us that the query took 1.279ms to execute. - -[role="screenshot"] -image::dev-tools/searchprofiler/images/overview.png["{searchprofiler} example"] - - -[NOTE] -==== -The Cumulative Time metric is the sum of individual shard times. -It is not necessarily the actual time it took for the query to return (wall clock time). -Because shards might be processed in parallel on multiple nodes, the wall clock time can -be significantly less than the Cumulative Time. However, if shards are colocated on the -same node and executed serially, the wall clock time is closer to the Cumulative Time. - -While the Cumulative Time metric is useful for comparing the performance of your -indices and shards, it doesn't necessarily represent the actual physical query times. -==== - -You can select the name of the shard and then click *View details* to see more profiling information, -including details about the query component(s) that ran on the shard, as well as the timing -breakdown of low-level Lucene methods. For more information, see {ref}/search-profile.html#profiling-queries[Profiling queries]. - -[float] -=== Index and type filtering - -By default, all queries executed by the {searchprofiler} are sent -to `GET /_search`. It searches across your entire cluster (all indices, all types). - -If you need to query a specific index or type (or several), you can use the Index -and Type filters. - -In the following example, the query is executed against the indices `test` and `kibana_1` -and the type `my_type`. This is equivalent making a request to `GET /test,kibana_1/my_type/_search`. - -[role="screenshot"] -image::dev-tools/searchprofiler/images/filter.png["Filtering by index and type"] diff --git a/docs/dev-tools/searchprofiler/gs-index.asciidoc b/docs/dev-tools/searchprofiler/gs-index.asciidoc deleted file mode 100644 index b4f5d48290f5e3..00000000000000 --- a/docs/dev-tools/searchprofiler/gs-index.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[role="xpack"] -[[xpack-profiler]] -= Profiling queries and aggregations - -[partintro] --- -{es} has a powerful {ref}/search-profile.html[Profile API] which can be used to inspect and analyze -your search queries. The response returns a large JSON blob, which can be -difficult to analyze manually. - -The {searchprofiler} tool can transform this JSON output -into a visualization that is easy to navigate, allowing you to diagnose and debug -poorly performing queries much faster. - -[role="screenshot"] -image::dev-tools/searchprofiler/images/overview.png["{searchprofiler} Visualization"] - --- - -include::getting-started.asciidoc[] diff --git a/docs/dev-tools/searchprofiler/images/filter.png b/docs/dev-tools/searchprofiler/images/filter.png index a740ec44b9d805..0bcfd7ca5cfad6 100644 Binary files a/docs/dev-tools/searchprofiler/images/filter.png and b/docs/dev-tools/searchprofiler/images/filter.png differ diff --git a/docs/dev-tools/searchprofiler/images/gs10.png b/docs/dev-tools/searchprofiler/images/gs10.png index 6be78b2ce8eb3e..e9a6615f50ac35 100644 Binary files a/docs/dev-tools/searchprofiler/images/gs10.png and b/docs/dev-tools/searchprofiler/images/gs10.png differ diff --git a/docs/dev-tools/searchprofiler/images/gs8.png b/docs/dev-tools/searchprofiler/images/gs8.png index 7ab8389897e4ef..75b93d4dfbdb76 100644 Binary files a/docs/dev-tools/searchprofiler/images/gs8.png and b/docs/dev-tools/searchprofiler/images/gs8.png differ diff --git a/docs/dev-tools/searchprofiler/images/overview.png b/docs/dev-tools/searchprofiler/images/overview.png index 19df1700a5bae1..2669adc13b349a 100644 Binary files a/docs/dev-tools/searchprofiler/images/overview.png and b/docs/dev-tools/searchprofiler/images/overview.png differ diff --git a/docs/dev-tools/searchprofiler/images/pasting.png b/docs/dev-tools/searchprofiler/images/pasting.png deleted file mode 100644 index 466ab9159bfed2..00000000000000 Binary files a/docs/dev-tools/searchprofiler/images/pasting.png and /dev/null differ diff --git a/docs/dev-tools/searchprofiler/images/search-profiler-json.png b/docs/dev-tools/searchprofiler/images/search-profiler-json.png new file mode 100644 index 00000000000000..a81286c9e6cca8 Binary files /dev/null and b/docs/dev-tools/searchprofiler/images/search-profiler-json.png differ diff --git a/docs/dev-tools/searchprofiler/index.asciidoc b/docs/dev-tools/searchprofiler/index.asciidoc index aca96dbfe3ee32..c323427318d549 100644 --- a/docs/dev-tools/searchprofiler/index.asciidoc +++ b/docs/dev-tools/searchprofiler/index.asciidoc @@ -1,20 +1,324 @@ [role="xpack"] [[xpack-profiler]] -== Profiling queries and aggregations +== Profile queries and aggregations -{es} has a powerful {ref}/search-profile.html[Profile API] which can be used to inspect and analyze +{es} has a powerful {ref}/search-profile.html[Profile API] that you can use to inspect and analyze your search queries. The response returns a large JSON blob, which can be difficult to analyze manually. -The {searchprofiler} tool can transform this JSON output +The *{searchprofiler}* tool can transform this JSON output into a visualization that is easy to navigate, allowing you to diagnose and debug poorly performing queries much faster. +[float] +[[search-profiler-getting-started]] +=== Get started -image::dev-tools/searchprofiler/images/overview.png["{searchprofiler} Visualization"] +*{searchprofiler}* is automatically enabled in {kib}. Open the main menu, +click *Dev Tools*, and then click *{searchprofiler}* +to get started. -include::getting-started.asciidoc[] +*{searchprofiler}* displays the names of the indices searched, the shards in each index, +and how long it took for the query to complete. To try it out, replace the default `match_all` query +with the query you want to profile, and then click *Profile*. -include::more-complicated.asciidoc[] +The following example shows the results of profiling the `match_all` query. +If you take a closer look at the information for the `.security_7` sample index, the +*Cumulative time* field shows you that the query took 0.028ms to execute. -include::pasting.asciidoc[] +[role="screenshot"] +image::dev-tools/searchprofiler/images/overview.png["{searchprofiler} visualization"] + + +[NOTE] +==== +The cumulative time metric is the sum of individual shard times. +It is not necessarily the actual time it took for the query to return (wall clock time). +Because shards might be processed in parallel on multiple nodes, the wall clock time can +be significantly less than the cumulative time. However, if shards are colocated on the +same node and executed serially, the wall clock time is closer to the cumulative time. + +While the cumulative time metric is useful for comparing the performance of your +indices and shards, it doesn't necessarily represent the actual physical query times. +==== + +To see more profiling information, click *View details*. You'll +see details about the query components that ran on the shard and the timing +breakdown of low-level methods. For more information, refer to {ref}/search-profile.html#profiling-queries[Profiling queries]. + +[float] +=== Filter for an index or type + +By default, all queries executed by the *{searchprofiler}* are sent +to `GET /_search`. It searches across your entire cluster (all indices, all types). + +To query a specific index or type, you can use the *Index* filter. + +In the following example, the query is executed against the indices `.security-7` and `kibana_sample_data_ecommerce`. +This is equivalent making a request to `GET /test,kibana_1/_search`. + +[role="screenshot"] +image::dev-tools/searchprofiler/images/filter.png["Filtering by index and type"] + +[[profile-complicated-query]] +[float] +=== Profile a more complicated query + +To understand how the query trees are displayed inside the *{searchprofiler}*, +take a look at a more complicated query. + +. Index the following data via *Console*: ++ +-- +[source,js] +-------------------------------------------------- +POST test/_bulk +{"index":{}} +{"name":"aaron","age":23,"hair":"brown"} +{"index":{}} +{"name":"sue","age":19,"hair":"red"} +{"index":{}} +{"name":"sally","age":19,"hair":"blonde"} +{"index":{}} +{"name":"george","age":19,"hair":"blonde"} +{"index":{}} +{"name":"fred","age":69,"hair":"blonde"} +-------------------------------------------------- +// CONSOLE +-- + +. From the *{searchprofiler}*, enter *test* in the *Index* field to restrict profiled +queries to the `test` index. + +. Replace the default `match_all` query in the query editor with a query that has two sub-query +components and includes a simple aggregation: ++ +-- +[source,js] +-------------------------------------------------- +{ + "query": { + "bool": { + "should": [ + { + "match": { + "name": "fred" + } + }, + { + "terms": { + "name": [ + "sue", + "sally" + ] + } + } + ] + } + }, + "aggs": { + "stats": { + "stats": { + "field": "price" + } + } + } +} +-------------------------------------------------- +// NOTCONSOLE +-- + +. Click *Profile* to profile the query and visualize the results. ++ +[role="screenshot"] +image::dev-tools/searchprofiler/images/gs8.png["Profiling the more complicated query"] ++ +- The top `BooleanQuery` component corresponds to the bool in the query. +- The second `BooleanQuery` corresponds to the terms query, which is internally +converted to a `Boolean` of should clauses. It has two child queries that correspond +to "sally" and "sue from the terms query. +- The `TermQuery` that's labeled with "name:fred" corresponds to match: fred in the query. ++ +If you look at the time columns, you can see that *Self time* and *Total time* are no longer +identical on all the rows. *Self time* represents how long the query component took to execute. +*Total time* is the time a query component and all its children took to execute. +Therefore, queries like the Boolean queries often have a larger total time than self time. + +. Click *Aggregation Profile* to view aggregation profiling statistics. ++ +This query includes a `stats` agg on the `"age"` field. +The *Aggregation Profile* tab is only enabled when the query being profiled contains an aggregation. + +. Click *View details* to view the timing breakdown. ++ +[role="screenshot"] +image::dev-tools/searchprofiler/images/gs10.png["Drilling into the first shard's details"] ++ +For more information about how the *{searchprofiler}* works, how timings are calculated, and +how to interpret various results, see +{ref}/search-profile.html#profiling-queries[Profiling queries]. + +[[profiler-render-JSON]] +[float] +=== Render pre-captured profiler JSON + +The *{searchprofiler}* queries the cluster that the {kib} node is attached to. +It does this by executing the query against the cluster and collecting the results. + +Sometimes you might want to investigate performance problems that are temporal in nature. +For example, a query might only be slow at certain time of day when many customers are using your system. +You can set up a process to automatically profile slow queries when they occur and then +save those profile responses for later analysis. + +The *{searchprofiler}* supports this workflow by allowing you to paste the +pre-captured JSON in the query editor. The *{searchprofiler}* will detect that you +have entered a JSON response (rather than a query) and will render just the visualization, +rather than querying the cluster. + +To see how this works, copy and paste the following profile response into the +query editor and click *Profile*. + +[source,js] +-------------------------------------------------- +{ + "took": 3, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "failed": 0 + }, + "hits": { + "total": 1, + "max_score": 1.3862944, + "hits": [ + { + "_index": "test", + "_type": "test", + "_id": "AVi3aRDmGKWpaS38wV57", + "_score": 1.3862944, + "_source": { + "name": "fred", + "age": 69, + "hair": "blonde" + } + } + ] + }, + "profile": { + "shards": [ + { + "id": "[O-l25nM4QN6Z68UA5rUYqQ][test][0]", + "searches": [ + { + "query": [ + { + "type": "BooleanQuery", + "description": "+name:fred #(ConstantScore(*:*))^0.0", + "time": "0.5884370000ms", + "breakdown": { + "score": 7243, + "build_scorer_count": 1, + "match_count": 0, + "create_weight": 196239, + "next_doc": 9851, + "match": 0, + "create_weight_count": 1, + "next_doc_count": 2, + "score_count": 1, + "build_scorer": 375099, + "advance": 0, + "advance_count": 0 + }, + "children": [ + { + "type": "TermQuery", + "description": "name:fred", + "time": "0.3016880000ms", + "breakdown": { + "score": 4218, + "build_scorer_count": 1, + "match_count": 0, + "create_weight": 132425, + "next_doc": 2196, + "match": 0, + "create_weight_count": 1, + "next_doc_count": 2, + "score_count": 1, + "build_scorer": 162844, + "advance": 0, + "advance_count": 0 + } + }, + { + "type": "BoostQuery", + "description": "(ConstantScore(*:*))^0.0", + "time": "0.1223030000ms", + "breakdown": { + "score": 0, + "build_scorer_count": 1, + "match_count": 0, + "create_weight": 17366, + "next_doc": 0, + "match": 0, + "create_weight_count": 1, + "next_doc_count": 0, + "score_count": 0, + "build_scorer": 102329, + "advance": 2604, + "advance_count": 2 + }, + "children": [ + { + "type": "MatchAllDocsQuery", + "description": "*:*", + "time": "0.03307600000ms", + "breakdown": { + "score": 0, + "build_scorer_count": 1, + "match_count": 0, + "create_weight": 6068, + "next_doc": 0, + "match": 0, + "create_weight_count": 1, + "next_doc_count": 0, + "score_count": 0, + "build_scorer": 25615, + "advance": 1389, + "advance_count": 2 + } + } + ] + } + ] + } + ], + "rewrite_time": 168640, + "collector": [ + { + "name": "CancellableCollector", + "reason": "search_cancelled", + "time": "0.02952900000ms", + "children": [ + { + "name": "SimpleTopScoreDocCollector", + "reason": "search_top_hits", + "time": "0.01931700000ms" + } + ] + } + ] + } + ], + "aggregations": [] + } + ] + } +} +-------------------------------------------------- +// NOTCONSOLE + +Your output should look similar to this: + +[role="screenshot"] +image::dev-tools/searchprofiler/images/search-profiler-json.png["Rendering pre-captured profiler JSON"] diff --git a/docs/dev-tools/searchprofiler/more-complicated.asciidoc b/docs/dev-tools/searchprofiler/more-complicated.asciidoc deleted file mode 100644 index 338341d65924dd..00000000000000 --- a/docs/dev-tools/searchprofiler/more-complicated.asciidoc +++ /dev/null @@ -1,104 +0,0 @@ -[role="xpack"] -[[profiler-complicated]] -=== Profiling a more complicated query - -To understand how the query trees are displayed inside the {searchprofiler}, -let's look at a more complicated query. - -. Index the following data via *Console*: -+ --- -[source,js] --------------------------------------------------- -POST test/_bulk -{"index":{}} -{"name":"aaron","age":23,"hair":"brown"} -{"index":{}} -{"name":"sue","age":19,"hair":"red"} -{"index":{}} -{"name":"sally","age":19,"hair":"blonde"} -{"index":{}} -{"name":"george","age":19,"hair":"blonde"} -{"index":{}} -{"name":"fred","age":69,"hair":"blonde"} --------------------------------------------------- -// CONSOLE --- - -. From the {searchprofiler}, enter "test" in the *Index* field to restrict profiled -queries to the `test` index. - -. Replace the default `match_all` query in the query editor with a query that has two sub-query -components and includes a simple aggregation: -+ --- -[source,js] --------------------------------------------------- -{ - "query": { - "bool": { - "should": [ - { - "match": { - "name": "fred" - } - }, - { - "terms": { - "name": [ - "sue", - "sally" - ] - } - } - ] - } - }, - "aggs": { - "stats": { - "stats": { - "field": "price" - } - } - } -} --------------------------------------------------- -// NOTCONSOLE --- - -. Click *Profile* to profile the query and visualize the results. -. Select the shard to view the query details. -+ -[role="screenshot"] -image::dev-tools/searchprofiler/images/gs8.png["Profiling the more complicated query"] - - -The detail view contains a row for each query component: - - - The top-level `BooleanQuery` component corresponds to the bool in the query. - - The second `BooleanQuery` corresponds to the terms query, which is internally - converted to a `Boolean` of should clauses. It has two child queries that correspond - to "sue" and "sally" from the terms query. - - The `TermQuery` that's labeled with "name:fred" corresponds to match: fred in the query. - -If you look at the time columns, you can see that "Self time" and "Total time" are no longer -identical on all the rows. Self time represents how long the query component took to execute. -Total time is the time a query component and all its children took to execute. -Therefore, queries like the Boolean queries often have a larger total time than self time. - - -==== Aggregations - -This particular query also includes a aggregation (a `stats` agg on the `"age"` field). -Click *Aggregation Profile* to view aggregation profiling statistics (this tab -is only enabled if the query being profiled contains an aggregation). - - -Select the name of the shard to view the aggregation details and timing breakdown. - -[role="screenshot"] -image::dev-tools/searchprofiler/images/gs10.png["Drilling into the first shard's details"] - -For more information about how the {searchprofiler} works, how timings are calculated, and -how to interpret various results, see -{ref}/search-profile.html#profiling-queries[Profiling queries]. diff --git a/docs/dev-tools/searchprofiler/pasting.asciidoc b/docs/dev-tools/searchprofiler/pasting.asciidoc deleted file mode 100644 index 9257a4d84fb562..00000000000000 --- a/docs/dev-tools/searchprofiler/pasting.asciidoc +++ /dev/null @@ -1,161 +0,0 @@ -[role="xpack"] -[[profiler-render]] -=== Rendering pre-captured profiler JSON - -The {searchprofiler} queries the cluster that the Kibana node is attached to. -It does this by executing the query against the cluster and collecting the results. - -But sometimes you may want to investigate performance problems that are temporal in nature. -For example, a query might only be slow at certain time of day when many customers are using your system. -You can setup a process to automatically profile slow queries when they occur and then -save those profile responses for later analysis. - -The {searchprofiler} supports this workflow by allowing you to paste the -pre-captured JSON in the query editor. The {searchprofiler} will detect that you -have entered a JSON response (rather than a query) and will just render the visualization, -rather than querying the cluster. - -To see how this works, copy and paste the following profile response into the -query editor and click *Profile*. - -[source,js] --------------------------------------------------- -{ - "took": 3, - "timed_out": false, - "_shards": { - "total": 1, - "successful": 1, - "failed": 0 - }, - "hits": { - "total": 1, - "max_score": 1.3862944, - "hits": [ - { - "_index": "test", - "_type": "test", - "_id": "AVi3aRDmGKWpaS38wV57", - "_score": 1.3862944, - "_source": { - "name": "fred", - "age": 69, - "hair": "blonde" - } - } - ] - }, - "profile": { - "shards": [ - { - "id": "[O-l25nM4QN6Z68UA5rUYqQ][test][0]", - "searches": [ - { - "query": [ - { - "type": "BooleanQuery", - "description": "+name:fred #(ConstantScore(*:*))^0.0", - "time": "0.5884370000ms", - "breakdown": { - "score": 7243, - "build_scorer_count": 1, - "match_count": 0, - "create_weight": 196239, - "next_doc": 9851, - "match": 0, - "create_weight_count": 1, - "next_doc_count": 2, - "score_count": 1, - "build_scorer": 375099, - "advance": 0, - "advance_count": 0 - }, - "children": [ - { - "type": "TermQuery", - "description": "name:fred", - "time": "0.3016880000ms", - "breakdown": { - "score": 4218, - "build_scorer_count": 1, - "match_count": 0, - "create_weight": 132425, - "next_doc": 2196, - "match": 0, - "create_weight_count": 1, - "next_doc_count": 2, - "score_count": 1, - "build_scorer": 162844, - "advance": 0, - "advance_count": 0 - } - }, - { - "type": "BoostQuery", - "description": "(ConstantScore(*:*))^0.0", - "time": "0.1223030000ms", - "breakdown": { - "score": 0, - "build_scorer_count": 1, - "match_count": 0, - "create_weight": 17366, - "next_doc": 0, - "match": 0, - "create_weight_count": 1, - "next_doc_count": 0, - "score_count": 0, - "build_scorer": 102329, - "advance": 2604, - "advance_count": 2 - }, - "children": [ - { - "type": "MatchAllDocsQuery", - "description": "*:*", - "time": "0.03307600000ms", - "breakdown": { - "score": 0, - "build_scorer_count": 1, - "match_count": 0, - "create_weight": 6068, - "next_doc": 0, - "match": 0, - "create_weight_count": 1, - "next_doc_count": 0, - "score_count": 0, - "build_scorer": 25615, - "advance": 1389, - "advance_count": 2 - } - } - ] - } - ] - } - ], - "rewrite_time": 168640, - "collector": [ - { - "name": "CancellableCollector", - "reason": "search_cancelled", - "time": "0.02952900000ms", - "children": [ - { - "name": "SimpleTopScoreDocCollector", - "reason": "search_top_hits", - "time": "0.01931700000ms" - } - ] - } - ] - } - ], - "aggregations": [] - } - ] - } -} --------------------------------------------------- -// NOTCONSOLE - -image::dev-tools/searchprofiler/images/pasting.png["Visualizing pre-collected responses"] 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 413b4aaf46b50d..96ca86cffbc5f0 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 00000000000000..916148141c8f32 --- /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 1b50d290945852..d17f9cb8a7ff37 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 41724f4914264e..afd66fbba18d3c 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 00000000000000..7d3b5296b5988f --- /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 00000000000000..0df643c6f133b4 --- /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 00000000000000..475b5f109d58c3 --- /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 00000000000000..3780a92053a5e5 --- /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 a66db46adf0f73..b03101b4d9fe64 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 b88a179c5c4b3a..042f2d1485618e 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 00000000000000..0a33be0d63f5c0 --- /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 00000000000000..df99d5ec4b831a --- /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 00000000000000..e9ad47b61419e7 --- /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 00000000000000..bbccea80b672fe --- /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 00000000000000..12a32b4544abaf --- /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 00000000000000..d14e3e4efa400a --- /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 00000000000000..bf458004b488bd --- /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 00000000000000..9864f67d70a43d --- /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 00000000000000..b4adf454a480f2 --- /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 00000000000000..c188f0ba0ce944 --- /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 d86f621231214e..4ade96691fb146 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 ebc2aeb419a753..517e8f3ffda1f1 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 4a203f10e7cd36..a26f8bd7b15944 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 d9796202d78785..b1fce06d46f3a7 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 fe55e131065ddd..9b4d1b022db2a9 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 76e4f222f02285..534f532850587e 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 90a19d53bd5e1e..9bc9d6d83674c3 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 b3e20bc7ed693a..f8d4c3f1b9d159 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 00000000000000..6e82132919f6dc --- /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 00000000000000..e4a252a3929499 --- /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 00000000000000..df851daab78068 --- /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 00000000000000..0ee2a26293e980 --- /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 00000000000000..89566b2ae66878 --- /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 00000000000000..7f158b46d1f064 --- /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 00000000000000..1ba079da032081 --- /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 00000000000000..bf503499b6298d --- /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 00000000000000..18fce3e273a390 --- /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 21fb7e3dfc7e87..488695475dcbe8 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 00000000000000..72e750e9be92a1 --- /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 cdb5664f96cddb..674cacc27a7e4f 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 00000000000000..bfe1925a21f68a --- /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 1388e04c315e23..38165a16833169 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 00000000000000..504f5e2df7d6ec --- /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 69f9d380422b66..dcdbd663f84e4e 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,14 +19,16 @@ 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 | | -| [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md) | boolean | | +| [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md) | boolean | The flag to toggle on emitting partial results. By default, the partial results are disabled. | | [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | RenderMode | | | [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableState | | | [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | string | | | [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md) | boolean | | +| [throttle](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md) | number | Throttling of partial results in milliseconds. By default, throttling is disabled. | | [uiState](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.uistate.md) | unknown | | | [variables](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.variables.md) | Record<string, any> | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md index 84c42c3f59f26d..8922b2d0f377e8 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md @@ -4,6 +4,8 @@ ## IExpressionLoaderParams.partial property +The flag to toggle on emitting partial results. By default, the partial results are disabled. + Signature: ```typescript diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md new file mode 100644 index 00000000000000..3383bce8797762 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [throttle](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md) + +## IExpressionLoaderParams.throttle property + +Throttling of partial results in milliseconds. By default, throttling is disabled. + +Signature: + +```typescript +throttle?: number; +``` 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 00000000000000..b4ceb8f96d698b --- /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 8503f81ad7d25d..3b308ca46ab0ae 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 04f564ab89857e..9a206f5f1bd1d0 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 f2042080f0a378..4c1ff2f2fddbd1 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 4a5f6d1a9fa99a..5ee2f019602489 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 c53a8cedff3366..26609392472fbc 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 65d168f5ca4af1..237dc4eb2683fe 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 8ce68047e0d765..f73cba203f8f12 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 07d34b8b49018f..42d46e6cbd5b5d 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 b9f96f1f0ceddf..a7ff9bf834e40d 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 bf22408729b084..bd13f800b7f335 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 9acbf86067fb42..f04abfa566fced 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 e42ed2416f79ef..64cfd87b7aac2b 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 b34119ca4a0390..e330036524ae70 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 d803acc49ce24f..18ff5f67eb5d83 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 242aa16709a9fd..5aca5a210139d5 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 38ea0987b120af..ebbd2e607eb5ab 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 097feb4dc90341..5b5dbc08d6e64b 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 f02672f531edf4..ce78aab933362e 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 fe4cfbb41fdc2d..5c6348d4e90fe6 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 60e2290c0bad7a..599ff2aa2e65ac 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 cf0a89fe1a79fd..10bc53d37785bb 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 0e93fe16b427d3..88a6f34e10e5cd 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 c71ca6f7534972..ded51feb294c13 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 508142f6e17b6c..5ce8c680e90b96 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 b59f14180b1ff2..a9fab17bdfda28 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 652583db785ad8..d71a1bb4a99d34 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 b471e238c1a0fd..26029fe58029f7 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 4777800ce5d578..abfc14b55e0200 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 66a23ee189ae1f..32736522d583a9 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 8df9402939b2e9..66b9cf2bca18c1 100644 Binary files a/docs/management/images/management-license.png and b/docs/management/images/management-license.png differ diff --git a/docs/management/images/tags/bulk-assign-selection.png b/docs/management/images/tags/bulk-assign-selection.png deleted file mode 100644 index 1c8687226b51fe..00000000000000 Binary files a/docs/management/images/tags/bulk-assign-selection.png and /dev/null differ diff --git a/docs/management/images/tags/create-tag.png b/docs/management/images/tags/create-tag.png deleted file mode 100644 index a88e754457b9f6..00000000000000 Binary files a/docs/management/images/tags/create-tag.png and /dev/null differ diff --git a/docs/management/images/tags/manage-assignments-flyout.png b/docs/management/images/tags/manage-assignments-flyout.png index a4e0b7a49d96a7..92a78be5f04028 100644 Binary files a/docs/management/images/tags/manage-assignments-flyout.png and b/docs/management/images/tags/manage-assignments-flyout.png differ diff --git a/docs/management/images/tags/tag-management-section.png b/docs/management/images/tags/tag-management-section.png index 4aae3ea067820c..34addfe4d326fd 100644 Binary files a/docs/management/images/tags/tag-management-section.png and b/docs/management/images/tags/tag-management-section.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 8d8efc7a6213eb..67a2a2cb299d2c 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/management/managing-tags.asciidoc b/docs/management/managing-tags.asciidoc index 88fdef66a74183..a0b3dce7f4b278 100644 --- a/docs/management/managing-tags.asciidoc +++ b/docs/management/managing-tags.asciidoc @@ -2,28 +2,26 @@ [[managing-tags]] == Tags -Tags enable you to categorize your saved objects. You can then easily filter for related objects based on shared tags. - -To begin, open the main menu, click *Stack Management*, then click *Tags*. +Tags enable you to categorize your saved objects. +You can then filter for related objects based on shared tags. [role="screenshot"] -image::images/tags/tag-management-section.png[Tags management section] +image::images/tags/tag-management-section.png[Tags management] [float] === Required permissions -Access to *Tags* requires the `Tag Management` {kib} privilege. To add the privilege, open the menu, -click *Stack Management*, then click *Roles*. - -In addition: +To create tags, you must meet the minimum requirements. +* Access to *Tags* requires the `Tag Management` Kibana privilege. To add the privilege, open the main menu, +and then click *Stack Management > Roles*. * The `read` privilege allows you to assign tags to the saved objects for which you have write permission. * The `write` privilege enables you to create, edit, and delete tags. - NOTE: Having the `Tag Management` {kib} privilege is not required to -view tags assigned on objects the user has `read` access to, or to filter objects by tags -in {kib} applications or from the navigational search. +view tags assigned on objects you have `read` access to, or to filter objects by tags +from the global search. + [float] [[settings-create-tag]] @@ -31,10 +29,9 @@ in {kib} applications or from the navigational search. Create a tag to assign to your saved objects. +. Open the main menu, and then click *Stack Management > Tags*. . Click *Create tag*. -+ -[role="screenshot"] -image::images/tags/create-tag.png[Tag creation popin] + . Enter a name and select a color for the new tag. + The name cannot be longer than 50 characters. @@ -42,33 +39,32 @@ The name cannot be longer than 50 characters. [float] [[settings-assign-tag]] -=== Assign a tag to saved objects +=== Assign a tag to an object -Assign or remove tags to one or more saved objects. You must have `write` permission +To assign and remove tags from saved objects, you must have `write` permission on the objects to which you assign the tags. -. Click the action (...) icon in the tag row, and then select the *Manage assignments* action. +. In the *Tags* view, find the tag you want to assign. +. Click the action menu (...) in the tag row, +and then select the *Manage assignments* action. + +. Select the objects to which you want to assign or remove tags. + [role="screenshot"] image::images/tags/manage-assignments-flyout.png[Assign flyout] -. Select the objects to which you want to assign or remove tags. -. Click on *Save tag assignments*. -TIP: To assign multiple tags to objects at once, select their checkboxes -and then select *Manage tag assignments* from the *selected tags* menu. +. Click *Save tag assignments*. -[role="screenshot"] -image::images/tags/bulk-assign-selection.png[Bulk assign tags] +TIP: To assign, delete, or clear multiple tags at once, +select their checkboxes in the *Tags* view, and then select +the desired action from the *selected tags* menu. [float] [[settings-delete-tag]] === Delete a tag -Delete a tag and remove it from any saved objects. +When you delete a tag, you remove it from all saved objects that use it. -. Click the action (...) icon in the tag row, and then select the *Delete* action. +. Click the action menu (...) in the tag row, and then select the *Delete* action. . Click *Delete tag*. - -TIP: To delete multiple tags at once, select their checkboxes in the list view, -and then select *Delete* action from the *selected tags* menu. \ No newline at end of file diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index eb3130ba6fdb5f..b49b669a007806 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -316,4 +316,21 @@ This content has moved. Refer to <> and <>. \ No newline at end of file +This content has moved. Refer to <>. + +[role="exclude",id="profiler-getting-started"] +== Getting start with Search Profiler + +This content has moved. Refer to <>. + + +[role="exclude",id="profiler-complicated"] +== Profiling a more complicated querying + +This content has moved. Refer to <>. + + +[role="exclude",id="profiler-render"] +== Rendering pre-captured profiler JSON + +This content has moved. Refer to <>. diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index d1d283ca60fbbb..a523c2cb005a21 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/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index dfb239f0e26c06..67de6f8d24960c 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -65,7 +65,7 @@ Changing these settings may disable features of the APM App. | Index name where Observability annotations are stored. Defaults to `observability-annotations`. | `xpack.apm.searchAggregatedTransactions` - | experimental[] Enables Transaction histogram metrics. Defaults to `false`. When `true`, additional configuration in APM Server is required. + | experimental[] Enables Transaction histogram metrics. Defaults to `auto` and the UI will use metric indices over transaction indices for transactions if aggregated transactions are found. When set to `always`, additional configuration in APM Server is required. When set to `never`, aggregated transactions are not used. See {apm-server-ref-v}/transaction-metrics.html[Configure transaction metrics] for more information. | `apm_oss.indexPattern` {ess-icon} diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index 134d9de3f49d88..cb80165e709904 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 a0dd8750ffc8f6..455ee76deefe30 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 7f4dbb3a96e6b0..fa89b7780e475f 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 5e18d934863aa9..880c98902983ff 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 b6d49cfaf8d3a5..279c2f5436c73e 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 7e253cdd0229d7..062d5ed88b3ca8 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 3a844b1c40de94..b346ca190055a9 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 782b44e383772c..2827d7e3367f84 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 0132337f4a7c66..f1852d3cc03b50 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 c78e0d14b0d4c8..9785dd9e77aba2 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 cb102b73f93b43..7559cd3c1b1b82 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 ac7a777eb05807..b0f27d45bb826e 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 00000000000000..0f3ab6c6eae2a6 --- /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 00000000000000..b8b5ceb1a9c6cf --- /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 00000000000000..364b7d5bfe8d32 --- /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 b03ee16d2f7463..fc91296ce37249 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 09cd26c9e620b9..7859758c274f3e 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 00000000000000..1db38e2240a869 --- /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/expression_functions/index.ts b/examples/preboot_example/server/config.ts similarity index 50% rename from src/plugins/expression_reveal_image/common/i18n/expression_functions/index.ts rename to examples/preboot_example/server/config.ts index 3d36b123421f48..db39f985bca30e 100644 --- a/src/plugins/expression_reveal_image/common/i18n/expression_functions/index.ts +++ b/examples/preboot_example/server/config.ts @@ -6,5 +6,13 @@ * Side Public License, v 1. */ -export * from './function_help'; -export * from './function_errors'; +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 00000000000000..0377250d0cf500 --- /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 00000000000000..a3c1e9d199143c --- /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 00000000000000..d18953eadf3307 --- /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 0875242a18da9c..85343fd8023d0e 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 b1b622381abb1c..d09c61a1c21105 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 a80680bd46dfca..514992891ad1b0 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 294caba4e7048b..08cf12343f459e 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-dev-utils/BUILD.bazel b/packages/kbn-dev-utils/BUILD.bazel index 9109f766e0e9f6..7c9beafc711ee5 100644 --- a/packages/kbn-dev-utils/BUILD.bazel +++ b/packages/kbn-dev-utils/BUILD.bazel @@ -43,6 +43,7 @@ NPM_MODULE_EXTRA_FILES = [ SRC_DEPS = [ "//packages/kbn-expect", + "//packages/kbn-std", "//packages/kbn-utils", "@npm//@babel/core", "@npm//axios", @@ -60,10 +61,12 @@ SRC_DEPS = [ "@npm//moment", "@npm//normalize-path", "@npm//rxjs", + "@npm//tar", "@npm//tree-kill", "@npm//tslib", "@npm//typescript", - "@npm//vinyl" + "@npm//vinyl", + "@npm//yauzl" ] TYPES_DEPS = [ @@ -76,8 +79,10 @@ TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/normalize-path", "@npm//@types/react", + "@npm//@types/tar", "@npm//@types/testing-library__jest-dom", - "@npm//@types/vinyl" + "@npm//@types/vinyl", + "@npm//@types/yauzl" ] DEPS = SRC_DEPS + TYPES_DEPS diff --git a/packages/kbn-dev-utils/src/extract.ts b/packages/kbn-dev-utils/src/extract.ts new file mode 100644 index 00000000000000..05ad2b4bd99ec2 --- /dev/null +++ b/packages/kbn-dev-utils/src/extract.ts @@ -0,0 +1,120 @@ +/* + * 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 Fs from 'fs/promises'; +import { createWriteStream } from 'fs'; +import Path from 'path'; +import { pipeline } from 'stream'; +import { promisify } from 'util'; + +import { lastValueFrom } from '@kbn/std'; +import Tar from 'tar'; +import Yauzl, { ZipFile, Entry } from 'yauzl'; +import * as Rx from 'rxjs'; +import { map, mergeMap, takeUntil } from 'rxjs/operators'; + +const asyncPipeline = promisify(pipeline); + +interface Options { + /** + * Path to the archive to extract, .tar, .tar.gz, and .zip archives are supported + */ + archivePath: string; + + /** + * Directory where the contents of the archive will be written. Existing files in that + * directory will be overwritten. If the directory doesn't exist it will be created. + */ + targetDir: string; + + /** + * Number of path segments to strip form paths in the archive, like --strip-components from tar + */ + stripComponents?: number; +} + +/** + * Extract tar and zip archives using a single function, supporting stripComponents + * for both archive types, only tested with familiar archives we create so might not + * support some weird exotic zip features we don't use in our own snapshot/build tooling + */ +export async function extract({ archivePath, targetDir, stripComponents = 0 }: Options) { + await Fs.mkdir(targetDir, { recursive: true }); + + if (archivePath.endsWith('.tar') || archivePath.endsWith('.tar.gz')) { + return await Tar.x({ + file: archivePath, + cwd: targetDir, + stripComponents, + }); + } + + if (!archivePath.endsWith('.zip')) { + throw new Error('unsupported archive type'); + } + + // zip mode + const zipFile = await new Promise((resolve, reject) => { + Yauzl.open(archivePath, { lazyEntries: true }, (error, _zipFile) => { + if (error || !_zipFile) { + reject(error || new Error('no zipfile provided by yauzl')); + } else { + resolve(_zipFile); + } + }); + }); + + // bound version of zipFile.openReadStream which returns an observable, because of type defs the readStream + // result is technically optional (thanks callbacks) + const openReadStream$ = Rx.bindNodeCallback(zipFile.openReadStream.bind(zipFile)); + + const close$ = Rx.fromEvent(zipFile, 'close'); + const error$ = Rx.fromEvent(zipFile, 'error').pipe( + takeUntil(close$), + map((error) => { + throw error; + }) + ); + + const entry$ = Rx.fromEvent(zipFile, 'entry').pipe( + takeUntil(close$), + mergeMap((entry) => { + const entryPath = entry.fileName.split(/\/|\\/).slice(stripComponents).join(Path.sep); + const fileName = Path.resolve(targetDir, entryPath); + + // detect directories + if (entry.fileName.endsWith('/')) { + return Rx.defer(async () => { + // ensure the directory exists + await Fs.mkdir(fileName, { recursive: true }); + // tell yauzl to read the next entry + zipFile.readEntry(); + }); + } + + // file entry + return openReadStream$(entry).pipe( + mergeMap(async (readStream) => { + if (!readStream) { + throw new Error('no readstream provided by yauzl'); + } + + // write the file contents to disk + await asyncPipeline(readStream, createWriteStream(fileName)); + // tell yauzl to read the next entry + zipFile.readEntry(); + }) + ); + }) + ); + + // trigger the initial 'entry' event, happens async so the event will be delivered after the observable is subscribed + zipFile.readEntry(); + + await lastValueFrom(Rx.merge(entry$, error$)); +} diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 3ac3927d25c056..9dc9d1723945a4 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -31,3 +31,4 @@ export * from './plugin_list'; export * from './plugins'; export * from './streams'; export * from './babel'; +export * from './extract'; diff --git a/packages/kbn-dev-utils/src/run/help.ts b/packages/kbn-dev-utils/src/run/help.ts index bd85922d00207d..ea197d18130861 100644 --- a/packages/kbn-dev-utils/src/run/help.ts +++ b/packages/kbn-dev-utils/src/run/help.ts @@ -8,6 +8,7 @@ import Path from 'path'; +import chalk from 'chalk'; import 'core-js/features/string/repeat'; import dedent from 'dedent'; @@ -116,7 +117,7 @@ export function getHelpForAllCommands({ : ''; return [ - dedent(command.usage || '') || command.name, + chalk.bold.whiteBright.bgBlack(` ${dedent(command.usage || '') || command.name} `), ` ${indent(dedent(command.description || 'Runs a dev task'), 2)}`, ...([indent(options, 2)] || []), ].join('\n'); diff --git a/packages/kbn-dev-utils/src/serializers/any_instance_serizlizer.ts b/packages/kbn-dev-utils/src/serializers/any_instance_serizlizer.ts index 122492d03a6f21..3bb69bcb580a36 100644 --- a/packages/kbn-dev-utils/src/serializers/any_instance_serizlizer.ts +++ b/packages/kbn-dev-utils/src/serializers/any_instance_serizlizer.ts @@ -6,9 +6,12 @@ * Side Public License, v 1. */ -export function createAnyInstanceSerializer(Class: Function, name?: string) { +export function createAnyInstanceSerializer( + Class: Function, + name?: string | ((instance: any) => string) +) { return { test: (v: any) => v instanceof Class, - serialize: () => `<${name ?? Class.name}>`, + serialize: (v: any) => `<${typeof name === 'function' ? name(v) : name ?? Class.name}>`, }; } diff --git a/packages/kbn-es/BUILD.bazel b/packages/kbn-es/BUILD.bazel index 48f0fb58e983fc..8d50d4e34e296e 100644 --- a/packages/kbn-es/BUILD.bazel +++ b/packages/kbn-es/BUILD.bazel @@ -39,9 +39,7 @@ DEPS = [ "@npm//glob", "@npm//node-fetch", "@npm//simple-git", - "@npm//tar-fs", "@npm//tree-kill", - "@npm//yauzl", "@npm//zlib" ] diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 52cfd0b71b8bae..32709fc608617e 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -13,18 +13,12 @@ const chalk = require('chalk'); const path = require('path'); const { downloadSnapshot, installSnapshot, installSource, installArchive } = require('./install'); const { ES_BIN } = require('./paths'); -const { - log: defaultLog, - parseEsLog, - extractConfigFiles, - decompress, - NativeRealm, -} = require('./utils'); +const { log: defaultLog, parseEsLog, extractConfigFiles, NativeRealm } = require('./utils'); const { createCliError } = require('./errors'); const { promisify } = require('util'); const treeKillAsync = promisify(require('tree-kill')); const { parseSettings, SettingsFilter } = require('./settings'); -const { CA_CERT_PATH, ES_P12_PATH, ES_P12_PASSWORD } = require('@kbn/dev-utils'); +const { CA_CERT_PATH, ES_P12_PATH, ES_P12_PASSWORD, extract } = require('@kbn/dev-utils'); const readFile = util.promisify(fs.readFile); // listen to data on stream until map returns anything but undefined @@ -144,13 +138,17 @@ exports.Cluster = class Cluster { this._log.info(chalk.bold(`Extracting data directory`)); this._log.indent(4); - // decompress excludes the root directory as that is how our archives are + // stripComponents=1 excludes the root directory as that is how our archives are // structured. This works in our favor as we can explicitly extract into the data dir const extractPath = path.resolve(installPath, extractDirName); this._log.info(`Data archive: ${archivePath}`); this._log.info(`Extract path: ${extractPath}`); - await decompress(archivePath, extractPath); + await extract({ + archivePath, + targetDir: extractPath, + stripComponents: 1, + }); this._log.indent(-4); } diff --git a/packages/kbn-es/src/install/archive.js b/packages/kbn-es/src/install/archive.js index 84c694f6fa9f0b..76db5a4427e6d0 100644 --- a/packages/kbn-es/src/install/archive.js +++ b/packages/kbn-es/src/install/archive.js @@ -12,7 +12,8 @@ const chalk = require('chalk'); const execa = require('execa'); const del = require('del'); const url = require('url'); -const { log: defaultLog, decompress } = require('../utils'); +const { extract } = require('@kbn/dev-utils'); +const { log: defaultLog } = require('../utils'); const { BASE_PATH, ES_CONFIG, ES_KEYSTORE_BIN } = require('../paths'); const { Artifact } = require('../artifact'); const { parseSettings, SettingsFilter } = require('../settings'); @@ -50,7 +51,11 @@ exports.installArchive = async function installArchive(archive, options = {}) { } log.info('extracting %s', chalk.bold(dest)); - await decompress(dest, installPath); + await extract({ + archivePath: dest, + targetDir: installPath, + stripComponents: 1, + }); log.info('extracted to %s', chalk.bold(installPath)); const tmpdir = path.resolve(installPath, 'ES_TMPDIR'); diff --git a/packages/kbn-es/src/utils/decompress.js b/packages/kbn-es/src/utils/decompress.js deleted file mode 100644 index c895f2f7986077..00000000000000 --- a/packages/kbn-es/src/utils/decompress.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const fs = require('fs'); -const path = require('path'); - -const yauzl = require('yauzl'); -const zlib = require('zlib'); -const tarFs = require('tar-fs'); - -function decompressTarball(archive, dirPath) { - return new Promise((resolve, reject) => { - fs.createReadStream(archive) - .on('error', reject) - .pipe(zlib.createGunzip()) - .on('error', reject) - .pipe(tarFs.extract(dirPath, { strip: true })) - .on('error', reject) - .on('finish', resolve); - }); -} - -function decompressZip(input, output) { - fs.mkdirSync(output, { recursive: true }); - return new Promise((resolve, reject) => { - yauzl.open(input, { lazyEntries: true }, (err, zipfile) => { - if (err) { - reject(err); - } - - zipfile.readEntry(); - - zipfile.on('close', () => { - resolve(); - }); - - zipfile.on('error', (err) => { - reject(err); - }); - - zipfile.on('entry', (entry) => { - const zipPath = entry.fileName.split(/\/|\\/).slice(1).join(path.sep); - const fileName = path.resolve(output, zipPath); - - if (/\/$/.test(entry.fileName)) { - fs.mkdirSync(fileName, { recursive: true }); - zipfile.readEntry(); - } else { - // file entry - zipfile.openReadStream(entry, (err, readStream) => { - if (err) { - reject(err); - } - - readStream.on('end', () => { - zipfile.readEntry(); - }); - - readStream.pipe(fs.createWriteStream(fileName)); - }); - } - }); - }); - }); -} - -exports.decompress = async function (input, output) { - const ext = path.extname(input); - - switch (path.extname(input)) { - case '.zip': - await decompressZip(input, output); - break; - case '.tar': - case '.gz': - await decompressTarball(input, output); - break; - default: - throw new Error(`unknown extension "${ext}"`); - } -}; diff --git a/packages/kbn-es/src/utils/decompress.test.js b/packages/kbn-es/src/utils/decompress.test.js deleted file mode 100644 index 0f9ac797e92076..00000000000000 --- a/packages/kbn-es/src/utils/decompress.test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const { decompress } = require('./decompress'); -const fs = require('fs'); -const path = require('path'); -const del = require('del'); -const os = require('os'); - -const fixturesFolder = path.resolve(__dirname, '__fixtures__'); -const randomDir = Math.random().toString(36); -const tmpFolder = path.resolve(os.tmpdir(), randomDir); -const dataFolder = path.resolve(tmpFolder, 'data'); -const esFolder = path.resolve(tmpFolder, '.es'); - -const zipSnapshot = path.resolve(dataFolder, 'snapshot.zip'); -const tarGzSnapshot = path.resolve(dataFolder, 'snapshot.tar.gz'); - -beforeEach(() => { - fs.mkdirSync(tmpFolder, { recursive: true }); - fs.mkdirSync(dataFolder, { recursive: true }); - fs.mkdirSync(esFolder, { recursive: true }); - - fs.copyFileSync(path.resolve(fixturesFolder, 'snapshot.zip'), zipSnapshot); - fs.copyFileSync(path.resolve(fixturesFolder, 'snapshot.tar.gz'), tarGzSnapshot); -}); - -afterEach(() => { - del.sync(tmpFolder, { force: true }); -}); - -test('zip strips root directory', async () => { - await decompress(zipSnapshot, path.resolve(esFolder, 'foo')); - expect(fs.readdirSync(path.resolve(esFolder, 'foo/bin'))).toContain('elasticsearch.bat'); -}); - -test('tar strips root directory', async () => { - await decompress(tarGzSnapshot, path.resolve(esFolder, 'foo')); - expect(fs.readdirSync(path.resolve(esFolder, 'foo/bin'))).toContain('elasticsearch'); -}); diff --git a/packages/kbn-es/src/utils/index.js b/packages/kbn-es/src/utils/index.js index 2715d7b675657e..ed83495e5310aa 100644 --- a/packages/kbn-es/src/utils/index.js +++ b/packages/kbn-es/src/utils/index.js @@ -11,7 +11,6 @@ exports.log = require('./log').log; exports.parseEsLog = require('./parse_es_log').parseEsLog; exports.findMostRecentlyChanged = require('./find_most_recently_changed').findMostRecentlyChanged; exports.extractConfigFiles = require('./extract_config_files').extractConfigFiles; -exports.decompress = require('./decompress').decompress; exports.NativeRealm = require('./native_realm').NativeRealm; exports.buildSnapshot = require('./build_snapshot').buildSnapshot; exports.archiveForPlatform = require('./build_snapshot').archiveForPlatform; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 2f6765cd57b9e1..49d4bdbc2de64b 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 48d36b706b8312..97a7f33be673d0 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 d0420d3c3699d9..65958d6669f73b 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 a3455d7ddf2b96..bc8418811e7aee 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/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts b/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts index 8b11387a1d0208..aab641367339ca 100644 --- a/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts @@ -6,89 +6,69 @@ * Side Public License, v 1. */ -/** - * Copied from src/core/server/elasticsearch/legacy/api_types.ts including its deprecation mentioned below - * TODO: Remove this and refactor the readPrivileges to utilize any newer client side ways rather than all this deprecated legacy stuff - */ -export interface LegacyCallAPIOptions { - /** - * Indicates whether `401 Unauthorized` errors returned from the Elasticsearch API - * should be wrapped into `Boom` error instances with properly set `WWW-Authenticate` - * header that could have been returned by the API itself. If API didn't specify that - * then `Basic realm="Authorization Required"` is used as `WWW-Authenticate`. - */ - wrap401Errors?: boolean; - /** - * A signal object that allows you to abort the request via an AbortController object. - */ - signal?: AbortSignal; -} - -type CallWithRequest, V> = ( - endpoint: string, - params: T, - options?: LegacyCallAPIOptions -) => Promise; +import { ElasticsearchClient } from '../elasticsearch_client'; export const readPrivileges = async ( - callWithRequest: CallWithRequest<{}, unknown>, + esClient: ElasticsearchClient, index: string ): Promise => { - return callWithRequest('transport.request', { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - cluster: [ - 'all', - 'create_snapshot', - 'manage', - 'manage_api_key', - 'manage_ccr', - 'manage_transform', - 'manage_ilm', - 'manage_index_templates', - 'manage_ingest_pipelines', - 'manage_ml', - 'manage_own_api_key', - 'manage_pipeline', - 'manage_rollup', - 'manage_saml', - 'manage_security', - 'manage_token', - 'manage_watcher', - 'monitor', - 'monitor_transform', - 'monitor_ml', - 'monitor_rollup', - 'monitor_watcher', - 'read_ccr', - 'read_ilm', - 'transport_client', - ], - index: [ - { - names: [index], - privileges: [ - 'all', - 'create', - 'create_doc', - 'create_index', - 'delete', - 'delete_index', - 'index', - 'manage', - 'maintenance', - 'manage_follow_index', - 'manage_ilm', - 'manage_leader_index', - 'monitor', - 'read', - 'read_cross_cluster', - 'view_index_metadata', - 'write', - ], - }, - ], - }, - }); + return ( + await esClient.transport.request({ + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: [ + 'all', + 'create_snapshot', + 'manage', + 'manage_api_key', + 'manage_ccr', + 'manage_transform', + 'manage_ilm', + 'manage_index_templates', + 'manage_ingest_pipelines', + 'manage_ml', + 'manage_own_api_key', + 'manage_pipeline', + 'manage_rollup', + 'manage_saml', + 'manage_security', + 'manage_token', + 'manage_watcher', + 'monitor', + 'monitor_transform', + 'monitor_ml', + 'monitor_rollup', + 'monitor_watcher', + 'read_ccr', + 'read_ilm', + 'transport_client', + ], + index: [ + { + names: [index], + privileges: [ + 'all', + 'create', + 'create_doc', + 'create_index', + 'delete', + 'delete_index', + 'index', + 'manage', + 'maintenance', + 'manage_follow_index', + 'manage_ilm', + 'manage_leader_index', + 'monitor', + 'read', + 'read_cross_cluster', + 'view_index_metadata', + 'write', + ], + }, + ], + }, + }) + ).body; }; diff --git a/src/core/TESTING.md b/src/core/TESTING.md index 10ead1ea8cfe22..5e0913e7a9d200 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 9c8e3e269ec883..8e0670472c80d1 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 d8148b0af807dc..071e61f17c25cb 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 b3dd3827352bd7..40304d27580ca8 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 ef919018f120b3..94c88f732f4e19 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 d62a4bcdd1e510..3f23889c57de6f 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 d23980ff55a2c9..673cb2e7ab5577 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 18a5eceb1b2d32..5131defc934616 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 0ba3446a772706..1af10b3ad981e2 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 8f9627a64b082c..0476f844d011f6 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 6088ec29db9985..1166c8f6b48c4d 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 ac793d960d03b8..2e80fbb9d20c0a 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 a417dbfeb43a3c..4140d75d1a1a10 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 f1006f93dbc2d4..372b9d4c0dfad9 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 c7a4721b7d2aea..040cb23e079887 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 5b672774c515a9..2d86281ce40d62 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 e705fff2e35a51..c9b1e0e7692e50 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 48dc485727251b..8b5a784b1f8a14 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 68a99ed3d41561..9e77786c1562cb 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 84f7ad07da2c3e..d12bafdef7a901 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 ad7af3ac8b84df..f6a9b653ec0345 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 e728cb0b824757..35a7c57b67610e 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 7c50e09b124688..6d37241c2a36fa 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 faa1c905afa9d5..a12e9e7d551881 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 dcb623c1ffc743..2ffe3b9d1e4cc3 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 1c7b7db305b7a9..86da1d94d3fc68 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 924f1584c5f8ff..ed23bf02d23ae5 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 ce4bd6fa2c59b4..18c9724c2f8f43 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 089028fc329761..0ccc0f51f6abd5 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 791ae2ab7abaa8..8932a4c73e1f2a 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 deb2d49f708176..f983a8b77fe085 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 94dc10ff4e8636..d97e3331c7cf5e 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 8bbf665cbc0965..375c7015b16d79 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 2bc5fa89b8e26d..ab620c790a157b 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 fb3ddaa77b4162..34647d090b9957 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 e652622049cfa1..f96b616256577a 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 01d5097887248c..886c0f667fdb4d 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 46a688c8abdf0a..e8408d37f40f43 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 71bf4bb96e1b0f..6c4a5606df152b 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 9b9ab0f48bacb0..0c213429e1951c 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 95a854f84d145d..b187283e27e34a 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 c9de5fb98eb02b..ade67d0dd2605e 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 04b78a84e818e4..0377525c741260 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 a589bc76d21fcd..ef5e1510837808 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 d8a7b542754801..8d29e3221a2ca3 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 0097aab82b21c2..4b9e45e271be2b 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 84fe5149c89c66..cad5a50dbc5050 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 99b63fc73687a1..e497f254e06329 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 0696deb9c07ae4..9c923943118a09 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 da8abe55b65922..e883cd59c8c77b 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 077e2f6e9c4850..c633db11edd7a4 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 62cb699bc49f6b..f7eee9580d11a7 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 00000000000000..7c2118bea725b0 --- /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 ecacbf0bfa0c23..0a30bfac85f5db 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 1b2b0b966d3a27..5bea371d479ae5 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 bbd296d6b1831a..7353f48b47194e 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 3a94de15d14b95..a2ca0aa2465828 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 8b24e05fc5bf4c..33ee6cc4e37484 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 44caa456e99559..6c295b152af3c2 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 3333574038ec7c..1ec02272d151f7 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 29859e95b63b2a..a199acd00eff5f 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 913d5ee4aaa99f..ad87b371aca338 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 0dffd8934a8cab..02a809b3c2c36b 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 d2a4b4bff33900..c77a3a967364c5 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 f3253e32aa08e8..540670274f4167 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 88c45962ce4a68..d8f9f035f44be3 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 9d17b289bfa4c2..ba6aec5f128c1f 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 b4eb98546de21b..ade10fc1c0257c 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 b40ce7a4e7b0e3..b560748026ace1 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 5f91c7b8734b89..75d358c8cbe6e1 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 341a04736b87a4..9817f08c59bf85 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 f5a4717fdbfaf4..6c3eee4981725d 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 19e3e6a6c68f98..93589648ca0ae4 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 ff844f44aede06..f423e40fbcaf98 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 f3a92c896b0148..3e410e4b19c0ef 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 b59418a67219e0..57640ec6acc0ac 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 f6028fa8b099d2..28f2ab799e0920 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 a71df00b39c5cf..1b655ccd8bd98f 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 a29fb01fbc0092..1b0caf7bf6255c 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 c90d2e804225c7..610bc1cac5a3be 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 ca7f11e28de75f..76ed4f8f24b3d4 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 4ba34ccb2149f0..7913bad3cad17d 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 70fd1c60efa614..29194b1e8fc62b 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 f4f2263a1bdb06..ee7b35a412e800 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 5c50df07dc6979..3cd645cb74ac00 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 99a9aaaddcb0bd..05bb60fb22c6db 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 26175ab2b68a9e..1e1264ba76a809 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 abcd00f4e2daf7..e61c9c2002a12b 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 f6327d4eabf43c..4a156c5fbb976d 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 0cdc806e997ef7..b0edcbdfd8677b 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 4f70f9d30b74bb..2b7f25538dcb1c 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 00000000000000..acdd9458a462d1 --- /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 00000000000000..dd4b1cb7d1df0b --- /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 00000000000000..4313541ef91d3a --- /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 00000000000000..61abc327c9ddbe --- /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 00000000000000..8d090d86446378 --- /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 00000000000000..63378046e80500 --- /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 d790e4a6799881..091d185cceefce 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 56131d77b23ebe..76558a999a3746 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 f6b39ea24262b4..495a38a1af5bf3 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 0abd8fd5a00576..67f8507b9b700f 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 81418c58175f5b..3d8213da62c6d9 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 bba0dc6fd8a67e..de7d21add6c6ca 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 fd4e1140d68b49..2d95822d922192 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(