diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c91d1a702b7eca..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 @@ -349,6 +351,21 @@ /x-pack/test/case_api_integration @elastic/security-threat-hunting /x-pack/plugins/lists @elastic/security-detections-response +## Security Solution sub teams - security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/public/management/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/public/common/lib/endpoint*/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/public/common/components/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/common/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/server/endpoint/routes/actions/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/server/endpoint/routes/metadata/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/server/endpoint/lib/policy/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/server/lib/license/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/server/fleet_integration/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/scripts/endpoint/event_filters/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/ @elastic/security-onboarding-and-lifecycle-mgt +/x-pack/test/security_solution_endpoint/apps/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt + # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index f4e62648a97413..f2359846504bf2 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -14,6 +14,7 @@ jobs: issue-mappings: | [ {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, + {"label": "Feature:Discover", "projectNumber": 44, "columnName": "Inbox"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, diff --git a/.i18nrc.json b/.i18nrc.json index 390e5e917d08e7..732644b43e1f7c 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -16,6 +16,7 @@ "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", + "expressionError": "src/plugins/expression_error", "expressionRevealImage": "src/plugins/expression_reveal_image", "inputControl": "src/plugins/input_control_vis", "inspector": "src/plugins/inspector", diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index c95a818ac47e44..e26c2ec09acf73 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -10,15 +10,15 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch Node.js rules http_archive( name = "build_bazel_rules_nodejs", - sha256 = "0fa2d443571c9e02fcb7363a74ae591bdcce2dd76af8677a95965edf329d778a", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.6.0/rules_nodejs-3.6.0.tar.gz"], + sha256 = "8f5f192ba02319254aaf2cdcca00ec12eaafeb979a80a1e946773c520ae0a2c9", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.7.0/rules_nodejs-3.7.0.tar.gz"], ) # Now that we have the rules let's import from them to complete the work load("@build_bazel_rules_nodejs//:index.bzl", "check_rules_nodejs_version", "node_repositories", "yarn_install") # Assure we have at least a given rules_nodejs version -check_rules_nodejs_version(minimum_version_string = "3.6.0") +check_rules_nodejs_version(minimum_version_string = "3.7.0") # Setup the Node.js toolchain for the architectures we want to support # 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/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 5f49360c926bfa..c1535e8a2146f0 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -72,6 +72,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a |This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. +|{kib-repo}blob/{branch}/src/plugins/expression_error/README.md[expressionError] +|Expression Error plugin adds an error renderer to the expression plugin. The renderer will display the error image. + + |{kib-repo}blob/{branch}/src/plugins/expression_reveal_image/README.md[expressionRevealImage] |Expression Reveal Image plugin adds a revealImage function to the expression plugin and an associated renderer. The renderer will display the given percentage of a given image. 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.exporters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exporters.md index 883dbcfe289cb2..efba24c008264d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exporters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.exporters.md @@ -10,5 +10,7 @@ exporters: { datatableToCSV: typeof datatableToCSV; CSV_MIME_TYPE: string; + cellHasFormulas: (val: string) => boolean; + tableHasFormulas: (columns: import("../../expressions").DatatableColumn[], rows: Record[]) => boolean; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md index 3e85859a3c831b..a2028bb5c12c60 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class FilterManager +export declare class FilterManager implements PersistableStateService ``` ## Constructors @@ -16,6 +16,16 @@ export declare class FilterManager | --- | --- | --- | | [(constructor)(uiSettings)](./kibana-plugin-plugins-data-public.filtermanager._constructor_.md) | | Constructs a new instance of the FilterManager class | +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [extract](./kibana-plugin-plugins-data-public.filtermanager.extract.md) | | any | | +| [getAllMigrations](./kibana-plugin-plugins-data-public.filtermanager.getallmigrations.md) | | () => {} | | +| [inject](./kibana-plugin-plugins-data-public.filtermanager.inject.md) | | any | | +| [migrateToLatest](./kibana-plugin-plugins-data-public.filtermanager.migratetolatest.md) | | any | | +| [telemetry](./kibana-plugin-plugins-data-public.filtermanager.telemetry.md) | | (filters: import("../../../../kibana_utils/common/persistable_state").SerializableState, collector: unknown) => {} | | + ## Methods | Method | Modifiers | Description | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntype.md new file mode 100644 index 00000000000000..46fd3a0725e403 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntype.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternType](./kibana-plugin-plugins-data-public.indexpatterntype.md) + +## IndexPatternType enum + +Signature: + +```typescript +export declare enum IndexPatternType +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| DEFAULT | "default" | | +| ROLLUP | "rollup" | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntypemeta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntypemeta.md index e6690b244c9ea5..19a884862d4609 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntypemeta.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntypemeta.md @@ -15,4 +15,5 @@ export interface TypeMeta | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.indexpatterntypemeta.aggs.md) | Record<string, AggregationRestrictions> | | +| [params](./kibana-plugin-plugins-data-public.indexpatterntypemeta.params.md) | {
rollup_index: string;
} | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntypemeta.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntypemeta.params.md new file mode 100644 index 00000000000000..12646a39188a07 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterntypemeta.params.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternTypeMeta](./kibana-plugin-plugins-data-public.indexpatterntypemeta.md) > [params](./kibana-plugin-plugins-data-public.indexpatterntypemeta.params.md) + +## IndexPatternTypeMeta.params property + +Signature: + +```typescript +params?: { + rollup_index: string; + }; +``` 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/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 65c4601d5faec9..7c2911875ee054 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -31,6 +31,7 @@ | --- | --- | | [BUCKET\_TYPES](./kibana-plugin-plugins-data-public.bucket_types.md) | | | [ES\_FIELD\_TYPES](./kibana-plugin-plugins-data-public.es_field_types.md) | \* | +| [IndexPatternType](./kibana-plugin-plugins-data-public.indexpatterntype.md) | | | [KBN\_FIELD\_TYPES](./kibana-plugin-plugins-data-public.kbn_field_types.md) | \* | | [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | | | [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md index de6f4563b678ac..7c850a89dff132 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md @@ -19,6 +19,7 @@ export interface QuerySuggestionGetFnArgs | [boolFilter](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.boolfilter.md) | any | | | [indexPatterns](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.indexpatterns.md) | IIndexPattern[] | | | [language](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.language.md) | string | | +| [method](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.method.md) | ValueSuggestionsMethod | | | [query](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.query.md) | string | | | [selectionEnd](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.selectionend.md) | number | | | [selectionStart](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.selectionstart.md) | number | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.method.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.method.md new file mode 100644 index 00000000000000..2bc9a4fba61c37 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querysuggestiongetfnargs.method.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) > [method](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.method.md) + +## QuerySuggestionGetFnArgs.method property + +Signature: + +```typescript +method?: ValueSuggestionsMethod; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md index ba2efcc9b75ca1..cef5abbbe3755f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md @@ -7,9 +7,9 @@ Signature: ```typescript -export declare function getCapabilitiesForRollupIndices(indices: { - [key: string]: any; -}): { +export declare function getCapabilitiesForRollupIndices(indices: Record): { [key: string]: any; }; ``` @@ -18,7 +18,7 @@ export declare function getCapabilitiesForRollupIndices(indices: { | Parameter | Type | Description | | --- | --- | --- | -| indices | {
[key: string]: any;
} | | +| indices | Record<string, {
rollup_jobs: any;
}> | | Returns: 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/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md index 2880e2d0d8f2cc..4befc100c10894 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md @@ -9,7 +9,5 @@ ```typescript mergeCapabilitiesWithFields: (rollupIndexCapabilities: { [key: string]: any; -}, fieldsFromFieldCapsApi: { - [key: string]: any; -}, previousFields?: FieldDescriptor[]) => FieldDescriptor[] +}, fieldsFromFieldCapsApi: Record, previousFields?: FieldDescriptor[]) => FieldDescriptor[] ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md index edaf1c9a9ce9eb..040bed5a8ce533 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md @@ -39,5 +39,5 @@ export declare class ExecutionN.B. input is initialized to null rather than undefined for legacy reasons, because in legacy interpreter it was set to null by default. | +| [start(input, isSubExpression)](./kibana-plugin-plugins-expressions-public.execution.start.md) | | Call this method to start execution.N.B. input is initialized to null rather than undefined for legacy reasons, because in legacy interpreter it was set to null by default. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md index 352226da6d72ad..b1fa6d7d518b99 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md @@ -11,7 +11,7 @@ N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons Signature: ```typescript -start(input?: Input): Observable>; +start(input?: Input, isSubExpression?: boolean): Observable>; ``` ## Parameters @@ -19,6 +19,7 @@ start(input?: Input): Observable> | Parameter | Type | Description | | --- | --- | --- | | input | Input | | +| isSubExpression | boolean | | Returns: 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.execution.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md index 47963e5e5ef46f..44d16ea02e2708 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md @@ -39,5 +39,5 @@ export declare class ExecutionN.B. input is initialized to null rather than undefined for legacy reasons, because in legacy interpreter it was set to null by default. | +| [start(input, isSubExpression)](./kibana-plugin-plugins-expressions-server.execution.start.md) | | Call this method to start execution.N.B. input is initialized to null rather than undefined for legacy reasons, because in legacy interpreter it was set to null by default. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md index 0eef7013cb3c61..23b4d414d09d14 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md @@ -11,7 +11,7 @@ N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons Signature: ```typescript -start(input?: Input): Observable>; +start(input?: Input, isSubExpression?: boolean): Observable>; ``` ## Parameters @@ -19,6 +19,7 @@ start(input?: Input): Observable> | Parameter | Type | Description | | --- | --- | --- | | input | Input | | +| isSubExpression | boolean | | Returns: 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/getting-started/images/add-sample-data.png b/docs/getting-started/images/add-sample-data.png index 9dee27dcde71b4..07a536b19d7d09 100644 Binary files a/docs/getting-started/images/add-sample-data.png and b/docs/getting-started/images/add-sample-data.png differ diff --git a/docs/getting-started/images/tutorial-discover-3.png b/docs/getting-started/images/tutorial-discover-3.png index b024ad6dc39fe8..79cf94058bb761 100644 Binary files a/docs/getting-started/images/tutorial-discover-3.png and b/docs/getting-started/images/tutorial-discover-3.png differ diff --git a/docs/getting-started/images/tutorial-discover-4.png b/docs/getting-started/images/tutorial-discover-4.png index 945a6155c02cd4..584221e8cfd04a 100644 Binary files a/docs/getting-started/images/tutorial-discover-4.png and b/docs/getting-started/images/tutorial-discover-4.png differ diff --git a/docs/getting-started/images/tutorial-final-dashboard.gif b/docs/getting-started/images/tutorial-final-dashboard.gif index 53b7bc04c5f656..6c82c4e53ca107 100644 Binary files a/docs/getting-started/images/tutorial-final-dashboard.gif and b/docs/getting-started/images/tutorial-final-dashboard.gif differ diff --git a/docs/getting-started/images/tutorial-sample-dashboard.png b/docs/getting-started/images/tutorial-sample-dashboard.png index 4c95c04c5e43ed..5e06009d0824e5 100644 Binary files a/docs/getting-started/images/tutorial-sample-dashboard.png and b/docs/getting-started/images/tutorial-sample-dashboard.png differ diff --git a/docs/getting-started/images/tutorial-sample-filter.png b/docs/getting-started/images/tutorial-sample-filter.png index 56ebacadbef45a..8823da311ebb54 100644 Binary files a/docs/getting-started/images/tutorial-sample-filter.png and b/docs/getting-started/images/tutorial-sample-filter.png differ diff --git a/docs/getting-started/images/tutorial-sample-filter2.png b/docs/getting-started/images/tutorial-sample-filter2.png index 21402feacdecde..4215b63d89fa1f 100644 Binary files a/docs/getting-started/images/tutorial-sample-filter2.png and b/docs/getting-started/images/tutorial-sample-filter2.png differ diff --git a/docs/getting-started/images/tutorial-sample-query.png b/docs/getting-started/images/tutorial-sample-query.png deleted file mode 100644 index 4f1ca24924b284..00000000000000 Binary files a/docs/getting-started/images/tutorial-sample-query.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-sample-query2.png b/docs/getting-started/images/tutorial-sample-query2.png deleted file mode 100644 index 0e91e1069a2012..00000000000000 Binary files a/docs/getting-started/images/tutorial-sample-query2.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-treemap.png b/docs/getting-started/images/tutorial-treemap.png deleted file mode 100644 index 32e14fd2308e31..00000000000000 Binary files a/docs/getting-started/images/tutorial-treemap.png and /dev/null differ diff --git a/docs/getting-started/images/tutorial-visualization-dropdown.png b/docs/getting-started/images/tutorial-visualization-dropdown.png index 29d1b99700964f..a069af95ed14a6 100644 Binary files a/docs/getting-started/images/tutorial-visualization-dropdown.png and b/docs/getting-started/images/tutorial-visualization-dropdown.png differ diff --git a/docs/getting-started/images/tutorial-visualization-treemap.png b/docs/getting-started/images/tutorial-visualization-treemap.png new file mode 100644 index 00000000000000..c6e8db133cb444 Binary files /dev/null and b/docs/getting-started/images/tutorial-visualization-treemap.png differ diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index d9835b312f3ee7..ed249008ac8de7 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -7,7 +7,7 @@ When you've finished, you'll know how to: * <> -* <> +* <> [float] === Required privileges @@ -24,125 +24,125 @@ include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] [[gs-get-data-into-kibana]] == Add the sample data -Sample data sets come with sample visualizations, dashboards, and more to help you explore {kib} without adding your own data. +Sample data sets come with sample visualizations, dashboards, and more to help you explore {kib} before you ingest or add your own data. -. From the home page, click *Try our sample data*. +. On the home page, click *Try our sample data*. . On the *Sample eCommerce orders* card, click *Add data*. + [role="screenshot"] -image::getting-started/images/add-sample-data.png[Add data UI] +image::images/add-sample-data.png[Add data UI for the sample data sets] [float] [[explore-the-data]] == Explore the data -*Discover* displays an interactive histogram that shows the distribution of of data, or documents, over time, and a table that lists the fields for each document that matches the index. By default, all fields are shown for each matching document. +*Discover* displays the data in an interactive histogram that shows the distribution of data, or documents, over time, and a table that lists the fields for each document that matches the index pattern. To view a subset of the documents, you can apply filters to the data, and customize the table to display only the fields you want to explore. . Open the main menu, then click *Discover*. . Change the <> to *Last 7 days*. + [role="screenshot"] -image::images/tutorial-discover-2.png[] +image::images/tutorial-discover-2.png[Time filter menu with Last 7 days filter configured] -. To focus in on the documents you want to view, use the <>. In the *KQL* search field, enter: +. To view the sales orders for women's clothing that are $60 or more, use the <> search field: + [source,text] -products.taxless_price >= 60 AND category : Women's Clothing -+ -The query returns the women's clothing orders for $60 and more. +products.taxless_price >= 60 and category : Women's Clothing + [role="screenshot"] -image::images/tutorial-discover-4.png[] +image::images/tutorial-discover-4.png[Discover tables that displays only the orders for women's clothing that are $60 or more] -. Hover over the list of *Available fields*, then click *+* next to the fields you want to view in the table. -+ -For example, when you add the *category* field, the table displays the product categories for the orders. +. To view only the product categories that contain sales orders, hover over the *category* field, then click *+*. + [role="screenshot"] -image::images/tutorial-discover-3.png[] -+ -For more information, refer to <>. +image::images/tutorial-discover-3.png[Discover table that displays only the product categories that contain orders] [float] [[view-and-analyze-the-data]] == View and analyze the data -A dashboard is a collection of panels that you can use to view and analyze the data. Panels contain visualizations, interactive controls, Markdown, and more. +A dashboard is a collection of panels that you can use to view and analyze the data. Panels contain visualizations, interactive controls, text, and more. . Open the main menu, then click *Dashboard*. . Click *[eCommerce] Revenue Dashboard*. + [role="screenshot"] -image::getting-started/images/tutorial-sample-dashboard.png[] +image::getting-started/images/tutorial-sample-dashboard.png[The [eCommerce] Revenue Dashboard that comes with the Sample eCommerce order data set] [float] -[[filter-and-query-the-data]] -=== Filter the data +[[create-a-visualization]] +=== Create a visualization panel + +Create a treemap panel that shows the top sales regions and manufacturers, then add the panel to the dashboard. -To focus in on the data you want to view on the dashboard, use filters. +. From the toolbar, click *Edit*, then click *Create visualzation*. -. From the *[eCommerce] Controls* panel, make a selection from the *Manufacturer* and *Category* dropdowns, then click *Apply changes*. +. Open the *Chart type* menu, then select *Treemap*. + -For example, the following dashboard shows the data for women's clothing from Gnomehouse. +[role="screenshot"] +image::getting-started/images/tutorial-visualization-dropdown.png[Chart type menu with Treemap selected] + +. From the *Available fields* list, drag and drop the following fields onto the workspace: + +* *geoip.city_name* + +* *manufacturer.keyword* + [role="screenshot"] -image::getting-started/images/tutorial-sample-filter.png[] +image::getting-started/images/tutorial-visualization-treemap.png[Treemap that displays Top values of geoip.city_name and Top values or manufacturer.keyword fields] -. To manually add a filter, click *Add filter*, then specify the options. +. Click *Save and return*. + -For example, to view the orders for Wednesday, select *day_of_week* from the *Field* dropdown, select *is* from the *Operator* dropdown, then select *Wednesday* from the *Value* dropdown. +The treemap appears as the last visualization panel on the dashboard. + [role="screenshot"] -image::getting-started/images/tutorial-sample-filter2.png[] +image::getting-started/images/tutorial-final-dashboard.gif[Final dashboard with new treemap visualization] + +[float] +[[interact-with-the-data]] +=== Interact with the data + +You can interact with the dashboard data using controls that allow you to apply dashboard-level filters. Interact with the *[eCommerce] Controls* panel to view the women's clothing data from the Gnomehouse manufacturer. -. When you are done, remove the filters. +. From the *Manufacturer* dropdown, select *Gnomehouse*. + +. From the *Category* dropdown, select *Women's Clothing*. + +. Click *Apply changes*. + -For more information, refer to <>. +[role="screenshot"] +image::getting-started/images/tutorial-sample-filter.png[The [eCommerce] Revenue Dashboard that shows only the women's clothing data from the Gnomehouse manufacturer] [float] -[[create-a-visualization]] -=== Create a visualization panel - -Create a treemap panel that shows the top regions and manufacturers, then add the panel to the dashboard. +[[filter-and-query-the-data]] +=== Filter the data -. From the toolbar, click *Edit*, then click *Create new*. +To view a subset of the data, you can apply filters to the dashboard panels. Apply a filter to view the women's clothing data generated on Wednesday from the Gnomehouse manufacturer. -. On the *New Visualization* window, click *Lens*. +. Click *Add filter*. -. From the *Available fields* list, drag and drop the following fields to the visualization builder: +. From the *Field* dropdown, select *day_of_week*. -* *geoip.city_name* +. From the *Operator* dropdown, select *is*. -* *manufacturer.keyword* -+ -. From the visualization dropdown, select *Treemap*. -+ -[role="screenshot"] -image::getting-started/images/tutorial-visualization-dropdown.png[Visualization dropdown with Treemap selected] +. From the *Value* dropdown, select *Wednesday*. . Click *Save*. - -. On the *Save Lens visualization*, enter a title and make sure *Add to Dashboard after saving* is selected, then click *Save and return*. -+ -The treemap appears as the last visualization panel on the dashboard. + [role="screenshot"] -image::getting-started/images/tutorial-final-dashboard.gif[Final dashboard with new treemap visualization] -+ -For more information, refer to <>. +image::getting-started/images/tutorial-sample-filter2.png[The [eCommerce] Revenue Dashboard that shows only the women's clothing data generated on Wednesday from the Gnomehouse manufacturer] [float] [[quick-start-whats-next]] == What's next? -If you are you ready to add your own data, refer to <>. +*Add your own data.* Ready to add your own data? Go to {fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack] to learn how to ingest your data, or go to <> and learn about all the other ways you can add data. -If you want to ingest your data, refer to {fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. +*Explore your own data in Discover.* Ready to learn more about exploring your data in *Discover*? Go to <>. -If you want to secure access to your data, refer to our guide on <> +*Create a dashboard with your own data.* Ready to learn more about analyzing your data in *Dashboard*? Go to <>. -If you want to try out {ml-features} with the sample data sets, refer to -{ml-docs}/ml-getting-started.html[Getting started with {ml}]. \ No newline at end of file +*Try out the {ml-features}.* Ready to analyze the sample data sets and generate models for its patterns of behavior? Go to {ml-docs}/ml-getting-started.html[Getting started with {ml}]. \ No newline at end of file diff --git a/docs/management/images/management-create-rollup-bar-chart.png b/docs/management/images/management-create-rollup-bar-chart.png index 324cfcb9ee5fb6..68ba4344c0ecf6 100644 Binary files a/docs/management/images/management-create-rollup-bar-chart.png and b/docs/management/images/management-create-rollup-bar-chart.png differ 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/management-rollup-index-pattern.png b/docs/management/images/management-rollup-index-pattern.png index 57ac00be7977c3..de7976e63f0503 100644 Binary files a/docs/management/images/management-rollup-index-pattern.png and b/docs/management/images/management-rollup-index-pattern.png differ diff --git a/docs/management/images/management-saved-objects.png b/docs/management/images/management-saved-objects.png index ba1f5af581dfd2..0ee720cfdb39d7 100644 Binary files a/docs/management/images/management-saved-objects.png and b/docs/management/images/management-saved-objects.png differ diff --git a/docs/management/images/management_create_rollup_job.png b/docs/management/images/management_create_rollup_job.png index c3139c9f8df1a6..f1dd1580c3a860 100644 Binary files a/docs/management/images/management_create_rollup_job.png and b/docs/management/images/management_create_rollup_job.png differ diff --git a/docs/management/images/management_rollup_job_dashboard.png b/docs/management/images/management_rollup_job_dashboard.png index d3c394183cad84..9573ab7a863e8e 100644 Binary files a/docs/management/images/management_rollup_job_dashboard.png and b/docs/management/images/management_rollup_job_dashboard.png differ diff --git a/docs/management/images/management_rollup_job_details.png b/docs/management/images/management_rollup_job_details.png index e6e93a6dae130c..5372ba4ad7d137 100644 Binary files a/docs/management/images/management_rollup_job_details.png and b/docs/management/images/management_rollup_job_details.png differ diff --git a/docs/management/images/management_rollup_job_vis.png b/docs/management/images/management_rollup_job_vis.png deleted file mode 100644 index e1f46e4db5c0a6..00000000000000 Binary files a/docs/management/images/management_rollup_job_vis.png and /dev/null differ diff --git a/docs/management/images/management_rollup_list.png b/docs/management/images/management_rollup_list.png index 60e9a350710038..505930bcb9a38a 100644 Binary files a/docs/management/images/management_rollup_list.png and b/docs/management/images/management_rollup_list.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-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 14d2ca63dd1436..5b39c6ad1c4cda 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -37,12 +37,15 @@ and select *Relationships*. [[managing-saved-objects-export-objects]] === Import and export -Using the import and export commands, you can move objects between different +Using the import and export actions, you can move objects between different {kib} instances. This action is useful when you have multiple environments for development and production. Import and export also work well when you have a large number of objects to update and want to batch the process. +In addition to the user interface, {kib} provides beta <> and <> APIs if +you want to automate this process. + [float] ==== Compatibility across versions diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index bde2ca472b258c..51821a935d3f57 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -64,13 +64,16 @@ You can read more at {ref}/rollup-job-config.html[rollup job configuration]. === Try it: Create and visualize rolled up data This example creates a rollup job to capture log data from sample web logs. -To follow along, add the sample web logs data set. +Before you start, <>. In this example, you want data that is older than 7 days in the target index pattern `kibana_sample_data_logs` -to roll up once a day into the index `rollup_logstash`. You’ll bucket the +to roll up into the `rollup_logstash` index. You’ll bucket the rolled up data on an hourly basis, using 60m for the time bucket configuration. This allows for more granular queries, such as 2h and 12h. +For this example, the job will perform the rollup every minute. However, you'd +typically roll up less frequently in production. + [float] ==== Create the rollup job @@ -80,7 +83,7 @@ As you walk through the *Create rollup job* UI, enter the data: |*Field* |*Value* |Name -|logs_job +|`logs_job` |Index pattern |`kibana_sample_data_logs` @@ -89,12 +92,13 @@ As you walk through the *Create rollup job* UI, enter the data: |`rollup_logstash` |Frequency -|Every day at midnight +|Every minute |Page size |1000 -|Delay (latency buffer)|7d +|Latency buffer +|7d |Date field |@timestamp @@ -118,6 +122,8 @@ As you walk through the *Create rollup job* UI, enter the data: |bytes (average) |=== +On the **Review and save** page, click **Start job now** and **Save**. + The terms, histogram, and metrics fields reflect the key information to retain in the rolled up data: where visitors are from (geo.src), what operating system they are using (machine.os.keyword), @@ -133,7 +139,6 @@ rollup index, or you can remove or archive it using < Index Patterns*. . Click *Create index pattern*, and select *Rollup index pattern* from the dropdown. @@ -149,7 +154,11 @@ is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_log matches the rolled up index pattern and `kibana_sample_data_logs` matches the index pattern for raw data. -. Open the main menu, click *Dashboard*, then create and add a vertical bar chart. +. Open the main menu, click *Dashboard*, then *Create dashboard*. + +. Set the <> to *Last 90 days*. + +. On the dashboard, click *Create visualization*. . Choose `rollup_logstash,kibana_sample_data_logs` as your source to see both the raw and rolled up data. @@ -157,13 +166,15 @@ as your source to see both the raw and rolled up data. [role="screenshot"] image::images/management-create-rollup-bar-chart.png[][Create visualization of rolled up data] -. Look at the data in your visualization. -+ -[role="screenshot"] -image::images/management_rollup_job_vis.png[][Visualization of rolled up data] +. Select *Bar vertical stacked* in the chart type dropdown. -. Optionally, create a dashboard that contains visualizations of the rolled up -data, raw data, or both. +. Add the `@timestamp` field to the *Horizontal axis*. + +. Add the `bytes` field to the *Vertical axis*, defaulting to an `Average of +bytes`. ++ +{kib} creates a vertical bar chart of your data. Select a section of the chart +to zoom in. + [role="screenshot"] image::images/management_rollup_job_dashboard.png[][Dashboard with rolled up data] diff --git a/docs/management/snapshot-restore/images/create-policy-example.png b/docs/management/snapshot-restore/images/create-policy-example.png old mode 100755 new mode 100644 index e871c925f5fd52..4ab5e438b306b4 Binary files a/docs/management/snapshot-restore/images/create-policy-example.png and b/docs/management/snapshot-restore/images/create-policy-example.png differ diff --git a/docs/management/snapshot-restore/images/create-policy.png b/docs/management/snapshot-restore/images/create-policy.png old mode 100755 new mode 100644 index d9a0dce0f41902..3ba33e2522bd5b Binary files a/docs/management/snapshot-restore/images/create-policy.png and b/docs/management/snapshot-restore/images/create-policy.png differ diff --git a/docs/management/snapshot-restore/images/create_snapshot.png b/docs/management/snapshot-restore/images/create_snapshot.png deleted file mode 100644 index 14c1229a23ce1a..00000000000000 Binary files a/docs/management/snapshot-restore/images/create_snapshot.png and /dev/null differ diff --git a/docs/management/snapshot-restore/images/register_repo.png b/docs/management/snapshot-restore/images/register_repo.png old mode 100755 new mode 100644 index 9e7ee9db4ce914..c742028ce108cf Binary files a/docs/management/snapshot-restore/images/register_repo.png and b/docs/management/snapshot-restore/images/register_repo.png differ diff --git a/docs/management/snapshot-restore/images/repository_list.png b/docs/management/snapshot-restore/images/repository_list.png old mode 100755 new mode 100644 index a4678e87bfb2cc..c4eb4fc1a3d1a8 Binary files a/docs/management/snapshot-restore/images/repository_list.png and b/docs/management/snapshot-restore/images/repository_list.png differ diff --git a/docs/management/snapshot-restore/images/restore-status.png b/docs/management/snapshot-restore/images/restore-status.png deleted file mode 100755 index fa48e32d2fef3f..00000000000000 Binary files a/docs/management/snapshot-restore/images/restore-status.png and /dev/null differ diff --git a/docs/management/snapshot-restore/images/snapshot-restore.png b/docs/management/snapshot-restore/images/snapshot-restore.png old mode 100755 new mode 100644 index 41a292f97c8536..8ca5dc95e58926 Binary files a/docs/management/snapshot-restore/images/snapshot-restore.png and b/docs/management/snapshot-restore/images/snapshot-restore.png differ diff --git a/docs/management/snapshot-restore/images/snapshot-retention.png b/docs/management/snapshot-restore/images/snapshot-retention.png old mode 100755 new mode 100644 index 7b390357a21b6b..44dfecc1a33218 Binary files a/docs/management/snapshot-restore/images/snapshot-retention.png and b/docs/management/snapshot-restore/images/snapshot-retention.png differ diff --git a/docs/management/snapshot-restore/images/snapshot_details.png b/docs/management/snapshot-restore/images/snapshot_details.png old mode 100755 new mode 100644 index 2bd226eecd84e3..e6c463d7acb7f0 Binary files a/docs/management/snapshot-restore/images/snapshot_details.png and b/docs/management/snapshot-restore/images/snapshot_details.png differ diff --git a/docs/management/snapshot-restore/images/snapshot_list.png b/docs/management/snapshot-restore/images/snapshot_list.png old mode 100755 new mode 100644 index dcbb43ec2ab842..f844bfddac4bea Binary files a/docs/management/snapshot-restore/images/snapshot_list.png and b/docs/management/snapshot-restore/images/snapshot_list.png differ diff --git a/docs/management/snapshot-restore/images/snapshot_permissions.png b/docs/management/snapshot-restore/images/snapshot_permissions.png deleted file mode 100644 index 463d4d6e389c61..00000000000000 Binary files a/docs/management/snapshot-restore/images/snapshot_permissions.png and /dev/null differ diff --git a/docs/management/snapshot-restore/index.asciidoc b/docs/management/snapshot-restore/index.asciidoc index 62633441ef1616..b041bd0873a058 100644 --- a/docs/management/snapshot-restore/index.asciidoc +++ b/docs/management/snapshot-restore/index.asciidoc @@ -2,8 +2,8 @@ [[snapshot-repositories]] == Snapshot and Restore -*Snapshot and Restore* enables you to backup your {es} -indices and clusters using data and state snapshots. +*Snapshot and Restore* lets you back up a running {es} +cluster using data and state snapshots. Snapshots are important because they provide a copy of your data in case something goes wrong. If you need to roll back to an older version of your data, you can restore a snapshot from the repository. @@ -34,17 +34,12 @@ The minimum required permissions to access *Snapshot and Restore* include: To add privileges, open the main menu, then click *Stack Management > Roles*. -[role="screenshot"] -image:management/snapshot-restore/images/snapshot_permissions.png["Edit Role"] - [float] [[kib-snapshot-register-repository]] === Register a repository A repository is where your snapshots live. You must register a snapshot repository before you can perform snapshot and restore operations. -If you don't have a repository, Kibana walks you through the process of -registering one. {kib} supports three repository types out of the box: shared file system, read-only URL, and source-only. For more information on these repositories and their settings, @@ -52,11 +47,9 @@ see {ref}/snapshots-register-repository.html[Repositories]. To use other repositories, such as S3, see {ref}/snapshots-register-repository.html#snapshots-repository-plugins[Repository plugins]. - -Once you create a repository, it is listed in the *Repositories* -view. -Click a repository name to view its type, number of snapshots, and settings, -and to verify status. +The *Repositories* view displays a list of registered repositories. Click a +repository name to view information about the repository, verify its status, or +clean it up. [role="screenshot"] image:management/snapshot-restore/images/repository_list.png["Repository list"] @@ -73,15 +66,8 @@ into each snapshot for further investigation. [role="screenshot"] image:management/snapshot-restore/images/snapshot_details.png["Snapshot details"] -If you don’t have any snapshots, you can create them from the {kib} <>. The -{ref}/snapshots-take-snapshot.html[snapshot API] -takes the current state and data in your index or cluster, and then saves it to a -shared repository. - -The snapshot process is "smart." Your first snapshot is a complete copy of -the data in your index or cluster. -All subsequent snapshots save the changes between the existing snapshots and -the new data. +If you don’t have any snapshots, you can create them using the +{ref}/create-snapshot-api.html[create snapshot API]. [float] [[kib-restore-snapshot]] @@ -93,14 +79,14 @@ restore a snapshot made from one cluster to another cluster. You might use the restore operation to: * Recover data lost due to a failure -* Migrate a current Elasticsearch cluster to a new version +* Migrate an {es} cluster to a new version * Move data from one cluster to another cluster To get started, go to the *Snapshots* view, find the snapshot, and click the restore icon in the *Actions* column. The Restore wizard presents options for the restore operation, including which -indices to restore and whether to modify the index settings. +data streams and indices to restore and whether to change index settings. You can restore an existing index only if it’s closed and has the same number of shards as the index in the snapshot. @@ -119,7 +105,7 @@ Use a {ref}/snapshot-lifecycle-management-api.html[snapshot lifecycle policy] to automate the creation and deletion of cluster snapshots. Taking automatic snapshots: -* Ensures your {es} indices and clusters are backed up on a regular basis +* Ensures your {es} data is backed up on a regular basis * Ensures a recent and relevant snapshot is available if a situation arises where a cluster needs to be recovered * Allows you to manage your snapshots in {kib}, instead of using a @@ -138,8 +124,8 @@ You can drill down into each policy to examine its settings and last successful You can perform the following actions on a snapshot policy: -* *Run* a policy immediately without waiting for the scheduled time. -This action is useful before an upgrade or before performing maintenance on indices. +* *Run* a policy immediately without waiting for the scheduled time. This action +is useful before an upgrade or before performing maintenance. * *Edit* a policy and immediately apply changes to the schedule. * *Delete* a policy to prevent any future snapshots from being taken. This action does not cancel any currently ongoing snapshots or remove any previously taken snapshots. @@ -160,7 +146,7 @@ and then click *Delete snapshots*. [role="xpack"] [[snapshot-restore-tutorial]] -=== Tutorial: Snapshot and Restore +=== Tutorial: Snapshot and Restore Ready to try *Snapshot and Restore*? In this tutorial, you'll learn to: @@ -174,15 +160,12 @@ Ready to try *Snapshot and Restore*? In this tutorial, you'll learn to: This example shows you how to register a shared file system repository and store snapshots. -Before you begin, you must register the location of the repository in the -{ref}/snapshots-register-repository.html#snapshots-filesystem-repository[path.repo] setting on -your master and data nodes. You can do this in one of two ways: -* Edit your `elasticsearch.yml` to include the `path.repo` setting. - -* Pass the `path.repo` setting when you start Elasticsearch. -+ -`bin/elasticsearch -E path.repo=/tmp/es-backups` +Before you begin, you must first mount the file system to the same location on +all master and data nodes. Then add the file system’s path or parent directory +to the +{ref}/snapshots-register-repository.html#snapshots-filesystem-repository[`path.repo`] +setting in `elasticsearch.yml` for each master and data node. [float] [[register-repo-example]] @@ -216,13 +199,10 @@ Use the {ref}/snapshots-take-snapshot.html[snapshot API] to create a snapshot. . Create the snapshot: + [source,js] -PUT /_snapshot/my_backup/2019-04-25_snapshot?wait_for_completion=true +PUT /_snapshot/my_backup/2099-04-25_snapshot?wait_for_completion=true + -In this example, the snapshot name is `2019-04-25_snapshot`. You can also +In this example, the snapshot name is `2099-04-25_snapshot`. You can also use {ref}/date-math-index-names.html[date math expression] for the snapshot name. -+ -[role="screenshot"] -image:management/snapshot-restore/images/create_snapshot.png["Create snapshot"] . Return to *Snapshot and Restore*. + @@ -251,16 +231,17 @@ image:management/snapshot-restore/images/create-policy-example.png["Create polic |Snapshot name |`` -|Schedule -|Every day at 1:30 a.m. - |Repository |`my_backup` +|Schedule +|Every day at 1:30 a.m. + |*Snapshot settings* | -|Indices -|Select the indices to back up. By default, all indices, including system indices, are backed up. +|Data streams and indices +|Select the data streams and indices to back up. By default, all data streams +and indices, including system indices, are backed up. |All other settings |Use the defaults. @@ -280,20 +261,22 @@ Your new policy is listed in the *Policies* view, and you see a summary of its d [[restore-snapshot-example]] ==== Restore a snapshot -Finally, you'll restore indices from an existing snapshot. +Finally, you'll restore data streams and indices from an existing snapshot. -. In the *Snapshots* view, find the snapshot you want to restore, for example `2019-04-25_snapshot`. +. In the *Snapshots* view, find the snapshot you want to restore, for example `2099-04-25_snapshot`. . Click the restore icon in the *Actions* column. . As you walk through the wizard, enter the following values: + |=== |*Logistics* | -|Indices -|Toggle to choose specific indices to restore, or leave in place to restore all indices. +|Data streams and indices +|Toggle to choose specific data streams and indices to restore. Use the default +to restore all data streams and indices in the snapshot. -|Rename indices -|Toggle to give your restored indices new names, or leave in place to restore under original index names. +|Rename data streams and indices +|Toggle to give your restored data streams and indices new names. Use the +default to restore the original data stream and index names. |All other fields |Use the defaults. @@ -313,4 +296,4 @@ or leave in place to keep existing settings. + The operation loads for a few seconds, and then you’re navigated to *Restore Status*, -where you can monitor the status of your restored indices. +where you can monitor the status of your restored data streams and indices. diff --git a/docs/management/watcher-ui/images/advanced-watch/advanced-watch-create.png b/docs/management/watcher-ui/images/advanced-watch/advanced-watch-create.png old mode 100755 new mode 100644 index 3cc9464b9b6819..f838ee911e9065 Binary files a/docs/management/watcher-ui/images/advanced-watch/advanced-watch-create.png and b/docs/management/watcher-ui/images/advanced-watch/advanced-watch-create.png differ diff --git a/docs/management/watcher-ui/images/advanced-watch/advanced-watch-simulate.png b/docs/management/watcher-ui/images/advanced-watch/advanced-watch-simulate.png old mode 100755 new mode 100644 index 10c535e3531d3c..dfbe3c71625035 Binary files a/docs/management/watcher-ui/images/advanced-watch/advanced-watch-simulate.png and b/docs/management/watcher-ui/images/advanced-watch/advanced-watch-simulate.png differ diff --git a/docs/management/watcher-ui/images/alerts-status.png b/docs/management/watcher-ui/images/alerts-status.png old mode 100755 new mode 100644 index 34fffc270e7b68..09a7d89b62a070 Binary files a/docs/management/watcher-ui/images/alerts-status.png and b/docs/management/watcher-ui/images/alerts-status.png differ diff --git a/docs/management/watcher-ui/images/execution-history.png b/docs/management/watcher-ui/images/execution-history.png old mode 100755 new mode 100644 index 3f7e1840eb8f9b..0c9afbf5ed3fe1 Binary files a/docs/management/watcher-ui/images/execution-history.png and b/docs/management/watcher-ui/images/execution-history.png differ diff --git a/docs/management/watcher-ui/images/threshold-alert/create-threshold-alert-created.png b/docs/management/watcher-ui/images/threshold-alert/create-threshold-alert-created.png old mode 100755 new mode 100644 index 9762452f767d57..f443ab42042586 Binary files a/docs/management/watcher-ui/images/threshold-alert/create-threshold-alert-created.png and b/docs/management/watcher-ui/images/threshold-alert/create-threshold-alert-created.png differ diff --git a/docs/management/watcher-ui/images/threshold-alert/threshold-alert-action.png b/docs/management/watcher-ui/images/threshold-alert/threshold-alert-action.png deleted file mode 100644 index 2972c530411573..00000000000000 Binary files a/docs/management/watcher-ui/images/threshold-alert/threshold-alert-action.png and /dev/null differ diff --git a/docs/management/watcher-ui/images/threshold-alert/threshold-alert-condition.png b/docs/management/watcher-ui/images/threshold-alert/threshold-alert-condition.png old mode 100755 new mode 100644 index 1189df4d23a0c8..d4619044b31128 Binary files a/docs/management/watcher-ui/images/threshold-alert/threshold-alert-condition.png and b/docs/management/watcher-ui/images/threshold-alert/threshold-alert-condition.png differ diff --git a/docs/management/watcher-ui/images/watches.png b/docs/management/watcher-ui/images/watches.png old mode 100755 new mode 100644 index 0161e37ab014d0..78a1ce725b159d Binary files a/docs/management/watcher-ui/images/watches.png and b/docs/management/watcher-ui/images/watches.png differ diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index 0bc6365918866a..4f68ac83d96226 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -123,9 +123,6 @@ in {es} for this example to work. you want to send the message when the condition is met. . Enter a subject and body for the email. -+ -[role="screenshot"] -image:management/watcher-ui/images/threshold-alert/threshold-alert-action.png["Action for threshold alert"] . To test the action before saving the watch, click *Send test email*. + diff --git a/docs/maps/images/gs_dashboard_with_map.png b/docs/maps/images/gs_dashboard_with_map.png index 49b71c16c12b2f..a4bf95948edf00 100644 Binary files a/docs/maps/images/gs_dashboard_with_map.png and b/docs/maps/images/gs_dashboard_with_map.png differ diff --git a/docs/maps/images/gs_dashboard_with_terms_filter.png b/docs/maps/images/gs_dashboard_with_terms_filter.png index 21b5c044cb35d9..bf84b2ee371af3 100644 Binary files a/docs/maps/images/gs_dashboard_with_terms_filter.png and b/docs/maps/images/gs_dashboard_with_terms_filter.png differ diff --git a/docs/maps/images/layer_search.png b/docs/maps/images/layer_search.png index 8e0e8ff6289531..d3828ed5f4551b 100644 Binary files a/docs/maps/images/layer_search.png and b/docs/maps/images/layer_search.png differ diff --git a/docs/maps/images/quantitative_data_driven_styling.png b/docs/maps/images/quantitative_data_driven_styling.png index a7852ed2020167..03dc22f433eeec 100644 Binary files a/docs/maps/images/quantitative_data_driven_styling.png and b/docs/maps/images/quantitative_data_driven_styling.png differ diff --git a/docs/maps/images/sample_data_ecommerce.png b/docs/maps/images/sample_data_ecommerce.png index 5b261bb5350225..7fba3da608d154 100644 Binary files a/docs/maps/images/sample_data_ecommerce.png and b/docs/maps/images/sample_data_ecommerce.png differ diff --git a/docs/maps/images/sample_data_web_logs.png b/docs/maps/images/sample_data_web_logs.png index f4f4de88f19921..e4902c3e896101 100644 Binary files a/docs/maps/images/sample_data_web_logs.png and b/docs/maps/images/sample_data_web_logs.png differ diff --git a/docs/maps/images/vector_style_class.png b/docs/maps/images/vector_style_class.png index 8c685dfcf0ab60..69549b9f5f2d82 100644 Binary files a/docs/maps/images/vector_style_class.png and b/docs/maps/images/vector_style_class.png differ diff --git a/docs/maps/images/vector_style_dynamic.png b/docs/maps/images/vector_style_dynamic.png index aeaef412b5220d..3032e74180afa0 100644 Binary files a/docs/maps/images/vector_style_dynamic.png and b/docs/maps/images/vector_style_dynamic.png differ diff --git a/docs/maps/images/vector_style_static.png b/docs/maps/images/vector_style_static.png index 47d9c3b21fcb6f..34908aa02fac73 100644 Binary files a/docs/maps/images/vector_style_static.png and b/docs/maps/images/vector_style_static.png differ 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 bd714c62ff5437..a0dd8750ffc8f6 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -337,24 +337,12 @@ For more details and a reference of audit events, refer to <>, specify where you want to write the audit events using `xpack.security.audit.appender`. - -[cols="2*<,*50"] -|====== -| `xpack.security.audit.appender` -| Optional. Specifies where audit logs should be written to and how they should be formatted. +| Set to `true` _and_ configure an appender with `xpack.security.audit.appender` to enable ECS audit logging`. *Default:* `false` 2+a| For example: - [source,yaml] ---------------------------------------- +xpack.security.audit.enabled: true xpack.security.audit.appender: type: rolling-file fileName: ./audit.log @@ -370,7 +358,31 @@ xpack.security.audit.appender: <1> Rotates log files every 24 hours. <2> Keeps maximum of 10 log files before deleting older ones. -| `xpack.security.audit.appender.type` +[NOTE] +============ +{ess} does not support custom log file policies. To enable audit logging on {ess} only specify: + +[source,yaml] +---------------------------------------- +xpack.security.audit.enabled: true +xpack.security.audit.appender.type: rolling-file +---------------------------------------- +============ + +[NOTE] +============ +deprecated:[7.15.0,"In 8.0 and later, the legacy audit logger will be removed, and this setting will enable the ECS audit logger with a default appender."] To enable the legacy audit logger only specify: + +[source,yaml] +---------------------------------------- +xpack.security.audit.enabled: true +---------------------------------------- +============ + +| `xpack.security.audit.appender` {ess-icon} +| Optional. Specifies where audit logs should be written to and how they should be formatted. + +| `xpack.security.audit.appender.type` {ess-icon} | Required. Specifies where audit logs should be written to. Allowed values are `console`, `file`, or `rolling-file`. Refer to <> and <> for appender specific settings. 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/change-space.png b/docs/spaces/images/change-space.png index fcf379afec84c9..7c55ed0c2d6a5d 100644 Binary files a/docs/spaces/images/change-space.png and b/docs/spaces/images/change-space.png differ diff --git a/docs/spaces/images/edit-space-feature-visibility.png b/docs/spaces/images/edit-space-feature-visibility.png index 3a74c21fe2635f..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 c4565a3ba0f4a4..9785dd9e77aba2 100644 Binary files a/docs/spaces/images/edit-space.png and b/docs/spaces/images/edit-space.png differ diff --git a/docs/spaces/images/securing-spaces.png b/docs/spaces/images/securing-spaces.png deleted file mode 100644 index e109e66b4cec6f..00000000000000 Binary files a/docs/spaces/images/securing-spaces.png and /dev/null differ diff --git a/docs/spaces/images/space-management.png b/docs/spaces/images/space-management.png index 9d68be0d19f0d9..2668758a98f4c8 100644 Binary files a/docs/spaces/images/space-management.png and b/docs/spaces/images/space-management.png differ diff --git a/docs/spaces/images/space-selector.png b/docs/spaces/images/space-selector.png deleted file mode 100644 index b2576cbc9acc4f..00000000000000 Binary files a/docs/spaces/images/space-selector.png and /dev/null differ diff --git a/docs/spaces/images/spaces-configure-landing-page.png b/docs/spaces/images/spaces-configure-landing-page.png index 15006594b6d7bc..335a64dc1c7431 100644 Binary files a/docs/spaces/images/spaces-configure-landing-page.png and b/docs/spaces/images/spaces-configure-landing-page.png differ diff --git a/docs/spaces/images/spaces-roles.png b/docs/spaces/images/spaces-roles.png index 407926895daa87..031b319cfad3e9 100644 Binary files a/docs/spaces/images/spaces-roles.png and b/docs/spaces/images/spaces-roles.png differ diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index 9501043eb24b68..aeeb7b45ccd8a7 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -12,7 +12,7 @@ spaces, you're asked to choose a space when you log in to Kibana. You can change current space at any time by using the menu. [role="screenshot"] -image::spaces/images/change-space.png["Change current space menu"] +image::images/change-space.png["Change current space menu"] Kibana supports spaces in several ways. You can: @@ -40,7 +40,7 @@ Open the main menu, then click *Stack Management > Spaces* for an overview of yo for you to create, edit, and delete spaces. [role="screenshot"] -image::spaces/images/space-management.png["Space management"] +image::images/space-management.png["Space management"] [float] ==== Create or edit a space @@ -57,7 +57,7 @@ You cannot change the space identifier once you create the space. if you prefer to create spaces programatically. [role="screenshot"] -image::spaces/images/edit-space.png["Space management"] +image::images/edit-space.png["Space management"] [float] ==== Delete a space @@ -81,7 +81,7 @@ to specific features on a per-user basis, you must configure <>. [role="screenshot"] -image::spaces/images/edit-space-feature-visibility.png["Controlling features visiblity"] +image::images/edit-space-feature-visibility.png["Controlling features visiblity"] [float] [[spaces-control-user-access]] @@ -95,26 +95,13 @@ while analysts or executives might have Dashboard and Canvas with read-only priv See <> for details. [role="screenshot"] -image::spaces/images/spaces-roles.png["Controlling features visiblity"] +image::images/spaces-roles.png["Controlling features visiblity"] [float] [[spaces-moving-objects]] === Move saved objects between spaces -To <> from one space to another, open the main menu, -then click *Stack Management > Saved Objects*. - -Alternately, you can move objects using {kib}'s <> -interface. - -. Navigate to the space that contains your saved objects. -. Export your saved objects. -. Navigate to the space where you want to import the objects. -. Import your saved objects. -. (Optional) Delete objects in the export space that you no longer need. - -{kib} also has beta <> and -<> APIs if you want to automate this process. +To move saved objects between spaces, you can <>, or <>. [float] [[spaces-default-route]] @@ -125,10 +112,10 @@ The landing page can route users to a specific dashboard, application, or saved To configure the landing page, use the default route setting in < {kib} > Advanced settings>>. -For example, you might set the default route to `/app/kibana#/dashboards`. +For example, you might set the default route to `/app/dashboards`. [role="screenshot"] -image::spaces/images/spaces-configure-landing-page.png["Configure space-level landing page"] +image::images/spaces-configure-landing-page.png["Configure space-level landing page"] [float] diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 2071f17ecff3d5..7d0e684e5f81bb 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -316,10 +316,16 @@ Each *Layer* in a visualization is associated with an index pattern and mutiple You can also change the index pattern for a single *Layer*. [float] -[[why-my-field-x-is-missing-from-the-fields-list]] -===== Why is my field X missing from the fields list? +[[why-my-field-is-missing-from-the-fields-list]] +===== Why is my field missing from the fields list? -*Lens* does not support the visualization of full-text fields, therefore it is not showing them in the data summary. +Fields do not appear in the *Available fields* in the following scenarios: +* The field is a full-text field. +* The field is a `geo_point` field +* The field is a `flattened` field. +* The field is a `object` field. + +Verify if the field appears in the *Empty fields* list. *Lens* uses heuristics to determine if the fields contain values. For sparse data sets, the heuristics are less precise. [float] [[how-to-handle-gaps-in-time-series-visualizations]] @@ -370,3 +376,9 @@ Here's a short list of few different aspects to check: ** If a custom `Number` configuration is used, check that the color stop values are covering the current data range. ** If a `Percent` configuration is used, and the data range changes, the colors displayed are affected. + +[float] +[[is-it-possible-to-show-icons-in-datatable]] +===== Is it possible to display icons in data tables? + +You can display icons with <> in data tables. diff --git a/docs/user/images/features-control.png b/docs/user/images/features-control.png deleted file mode 100755 index abe75d5ab6fc14..00000000000000 Binary files a/docs/user/images/features-control.png and /dev/null differ diff --git a/docs/user/images/select-your-space.png b/docs/user/images/select-your-space.png old mode 100755 new mode 100644 index 887e8eea27c5c4..2580455162fec9 Binary files a/docs/user/images/select-your-space.png and b/docs/user/images/select-your-space.png differ diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 82ca11f2162fda..783fa2b1c521f4 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -206,7 +206,7 @@ image::images/rules-and-connectors.png[Rules and Connectors view] === Organize your work in spaces Want to share {kib}’s goodness with other people or teams without overwhelming them? You can do so -with <>, built for organizing your visualizations, dashboards, and indices. +with <>, built for organizing your visualizations, dashboards, and index patterns. Think of a space as its own mini {kib} installation—it’s isolated from all other spaces, so you can tailor it to your specific needs without impacting others. @@ -234,15 +234,15 @@ For example, roles with no access to an app will not have access to its alerts. ==== Control feature visibility You can take spaces one step further and control which features are visible -within each space. For example, you might hide **Dev Tools** in your "Executive" -space or show **Stack Monitoring** only in your "Admin" space. +within each space. For example, you might hide **Dev Tools** in your "Marketing" +space or show **Stack Monitoring** only in your "Engineering" space. Controlling feature visibility is not a security feature. To secure access to specific features on a per-user basis, you must configure <>. [role="screenshot"] -image::images/features-control.png[Features Controls view] +image::spaces/images/edit-space-feature-visibility.png[Features Controls view] [float] [[intro-kibana-Security]] @@ -260,7 +260,7 @@ see <>. allowing you to login using {es}’s built-in realms, or by your own single sign-on provider. [role="screenshot"] -image::images/login-screen.png[Login page] +image::security/images/kibana-login.png[Login page] [float] ==== Secure access @@ -281,7 +281,7 @@ while analysts or executives might have *Dashboard* and *Canvas* with read-only levels, or you can automate role creation via our <>. [role="screenshot"] -image::images/roles-and-privileges.png[{kib privileges}] +image::spaces/images/spaces-roles.png[{kib privileges}] [float] ==== Audit access diff --git a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc index 8f2c8d106c77cf..3321a9d0c02a15 100644 --- a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc +++ b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc @@ -57,8 +57,12 @@ xpack.task_manager.monitored_task_execution_thresholds: The health API is best consumed by via the `/api/task_manager/_health` endpoint. -Additionally, the metrics are logged in the {kib} `DEBUG` logger at a regular cadence. -To enable Task Manager DEBUG logging in your {kib} instance, add the following to your `kibana.yml`: +Additionally, there are two ways to consume these metrics: + +*Debug logging* + +The metrics are logged in the {kib} `DEBUG` logger at a regular cadence. +To enable Task Manager debug logging in your {kib} instance, add the following to your `kibana.yml`: [source,yml] ---- @@ -69,7 +73,22 @@ logging: level: debug ---- -These stats are logged based the number of milliseconds set in your <> setting, which means it could add substantial noise to your logs. Only enable this level of logging temporarily. +These stats are logged based on the number of milliseconds set in your <> setting, which could add substantial noise to your logs. Only enable this level of logging temporarily. + +*Automatic logging* + +By default, the health API runs at a regular cadence, and each time it runs, it attempts to self evaluate its performance. If this self evaluation yields a potential problem, +a message will log to the {kib} server log. In addition, the health API will look at how long tasks have waited to start (from when they were scheduled to start). If this number exceeds a configurable threshold (<>), the same message as above will log to the {kib} server log. + +This message looks like: + +[source,log] +---- +Detected potential performance issue with Task Manager. Set 'xpack.task_manager.monitored_stats_health_verbose_log.enabled: true' in your Kibana.yml to enable debug logging` +---- + + +If this message appears, set <> to `true` in your `kibana.yml`. This will start logging the health metrics at either a `warn` or `error` log level, depending on the detected severity of the potential problem. [float] [[making-sense-of-task-manager-health-stats]] diff --git a/docs/user/security/access-agreement.asciidoc b/docs/user/security/access-agreement.asciidoc index 362eb805012103..9d9a0bb61a90b0 100644 --- a/docs/user/security/access-agreement.asciidoc +++ b/docs/user/security/access-agreement.asciidoc @@ -2,14 +2,16 @@ [[xpack-security-access-agreement]] === Access agreement -Some work environments require you to acknowledge and accept an agreement before you can access {kib}, which can contain sensitive information. The agreement text supports Markdown format and can be specified using the `xpack.security.authc.providers...accessAgreement.message` setting. +Access agreement is a https://www.elastic.co/subscriptions[subscription feature] that requires users to acknowledge and accept an +agreement before accessing {kib}. The agreement text supports Markdown format and can be specified using the +`xpack.security.authc.providers...accessAgreement.message` setting. [NOTE] ============================================================================ You need to acknowledge the access agreement only once per session, and {kib} reports the acknowledgement in the audit logs. ============================================================================ -Here is how your `kibana.yml` can look like if you define an access agreement: +Here is an example of defining an access agreement in `kibana.yml`: [source,yaml] -------------------------------------------------------------------------------- @@ -17,11 +19,14 @@ xpack.security.authc.providers: basic.basic1: order: 0 accessAgreement: - message: "**You are accessing a system with a sensitive information** \n\n - By logging in, you acknowledge that (shortened ...)" + message: | + **You are accessing a system with sensitive information** + + By logging in, you acknowledge that information system usage + ...(shortened) -------------------------------------------------------------------------------- When you authenticate using `basic.basic1`, you'll see the following agreement that you must acknowledge before you can access {kib}: [role="screenshot"] -image::user/security/images/access-agreement.png["Access Agreement UI"] +image::images/access-agreement.png["Access Agreement UI"] diff --git a/docs/user/security/api-keys/images/api-keys.png b/docs/user/security/api-keys/images/api-keys.png index ec4111a4432534..2a9df066fc3b8a 100644 Binary files a/docs/user/security/api-keys/images/api-keys.png and b/docs/user/security/api-keys/images/api-keys.png differ diff --git a/docs/user/security/api-keys/images/create-api-key.png b/docs/user/security/api-keys/images/create-api-key.png index b0041f69b05b69..f80bd10db43d44 100644 Binary files a/docs/user/security/api-keys/images/create-api-key.png and b/docs/user/security/api-keys/images/create-api-key.png differ diff --git a/docs/user/security/api-keys/index.asciidoc b/docs/user/security/api-keys/index.asciidoc index 6b92ab3c6656a0..94301568b64385 100644 --- a/docs/user/security/api-keys/index.asciidoc +++ b/docs/user/security/api-keys/index.asciidoc @@ -17,7 +17,7 @@ remote sources, without a live user interaction. To manage API keys, open the main menu, then click *Stack Management > API Keys*. [role="screenshot"] -image:user/security/api-keys/images/api-keys.png["API Keys UI"] +image:images/api-keys.png["API Keys UI"] [float] [[api-keys-service]] @@ -49,7 +49,7 @@ cluster privileges to use API keys in {kib}. To manage roles, open the main menu To create an API key, open the main menu, then click *Stack Management > API Keys > Create API key*. [role="screenshot"] -image:user/security/api-keys/images/create-api-key.png["Create API Key UI"] +image:images/create-api-key.png["Create API Key UI"] Once created, you can copy the API key (Base64 encoded) and use it to send requests to {es} on your behalf. For example: diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index 5808e56d6d2897..e2f21e3f8470cb 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -2,9 +2,9 @@ [[xpack-security-audit-logging]] === Audit logs -You can enable auditing to keep track of security-related events such as -authorization success and failures. Logging these events enables you to monitor -{kib} for suspicious activity and provides evidence in the event of an attack. +Audit logging is a https://www.elastic.co/subscriptions[subscription feature] that you can enable to keep track of security-related events, +such as authorization success and failures. Logging these events enables you to monitor {kib} for suspicious activity and provides evidence +in the event of an attack. Use the {kib} audit logs in conjunction with {ref}/enable-audit-logging.html[{es} audit logging] to get a holistic view of all security related events. {kib} defers to the {es} security @@ -14,16 +14,24 @@ by cluster-wide privileges. For more information on enabling audit logging in [IMPORTANT] ============================================================================ +Kibana offers two audit logs: a **deprecated** legacy audit logger, and a new +ECS-compliant audit logger. We strongly advise using the <>, +as the legacy audit logger will be removed in an upcoming version. +============================================================================ + +[NOTE] +============================================================================ Audit logs are **disabled** by default. To enable this functionality, you must -set `xpack.security.audit.enabled` to `true` in `kibana.yml`. +set `xpack.security.audit.enabled` to `true` in `kibana.yml`, and configure +an <> to write the audit log to a location of your choosing. ============================================================================ -The current version of the audit logger uses the standard {kib} logging output, +The legacy audit logger uses the standard {kib} logging output, which can be configured in `kibana.yml`. For more information, refer to <>. -The audit logger uses a separate logger and can be configured using +The <> uses a separate logger and can be configured using the options in <>. -==== Audit event types +==== Legacy audit event types When you are auditing security events, each request can generate multiple audit events. The following is a list of the events that can be generated: @@ -42,7 +50,7 @@ events. The following is a list of the events that can be generated: ============================================================================ The following events are only logged if the ECS audit logger is enabled. For information on how to configure `xpack.security.audit.appender`, refer to -<>. +<>. ============================================================================ Refer to the table of events that can be logged for auditing purposes. diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 5506e7ab375a2f..bc564308c057ea 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -61,7 +61,7 @@ xpack.security.authc.providers: -------------------------------------------------------------------------------- [role="screenshot"] -image::user/security/images/kibana-login.png["Login Selector UI"] +image::security/images/kibana-login.png["Login Selector UI"] For more information, refer to <>. @@ -82,7 +82,9 @@ For more information about basic authentication and built-in users, see [[token-authentication]] ==== Token authentication -Token authentication allows users to log in using the same {kib} provided login form as basic authentication, and is based on the Native security realm or LDAP security realm that is provided by {es}. The token authentication provider is built on {es} token APIs. +Token authentication is a https://www.elastic.co/subscriptions[subscription feature]. This allows users to log in using the same {kib} +provided login form as basic authentication, and is based on the Native security realm or LDAP security realm that is provided by {es}. The +token authentication provider is built on {es} token APIs. Prior to configuring {kib}, ensure token support is enabled in {es}. See the {ref}/security-api-get-token.html[{es} token API] documentation for more information. @@ -107,7 +109,11 @@ Switching to the token authentication provider from basic one will make {kib} to PKI authentication will not work if {kib} is hosted behind a TLS termination reverse proxy. In this configuration, {kib} does not have direct access to the client certificates and cannot authenticate the user. ============================================================================ -PKI authentication allows users to log into {kib} using X.509 client certificates that must be presented while connecting to {kib}. The certificates must first be accepted for authentication on the {kib} TLS layer, and then they are further validated by an {es} PKI realm. The PKI authentication provider relies on the {es} {ref}/security-api-delegate-pki-authentication.html[Delegate PKI authentication API] to exchange X.509 client certificates to access tokens. All subsequent requests to {es} APIs on behalf of users will be authenticated using these access tokens. +PKI authentication is a https://www.elastic.co/subscriptions[subscription feature]. This allows users to log +into {kib} using X.509 client certificates that must be presented while connecting to {kib}. The certificates must first be accepted for +authentication on the {kib} TLS layer, and then they are further validated by an {es} PKI realm. The PKI authentication provider relies on +the {es} {ref}/security-api-delegate-pki-authentication.html[Delegate PKI authentication API] to exchange X.509 client certificates to +access tokens. All subsequent requests to {es} APIs on behalf of users will be authenticated using these access tokens. Prior to configuring {kib}, ensure that the PKI realm is enabled in {es} and configured to permit delegation. See {ref}/configuring-pki-realm.html[Configuring a PKI realm] for more information. @@ -144,9 +150,11 @@ Note that with `server.ssl.clientAuthentication` set to `required`, users are as [[saml]] ==== SAML single sign-on -SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {ref}/saml-guide.html[Configuring SAML single sign-on on the Elastic Stack]. +SAML authentication is part of single sign-on (SSO), a https://www.elastic.co/subscriptions[subscription feature]. This allows users to log +in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting +it up in {kib}. See {ref}/saml-guide.html[Configuring SAML single sign-on on the Elastic Stack]. -Enable the SAML authentication specifying which SAML realm in {es} should be used: +Enable SAML authentication by specifying which SAML realm in {es} should be used: [source,yaml] -------------------------------------------------------------------------------- @@ -156,7 +164,7 @@ xpack.security.authc.providers: realm: saml1 -------------------------------------------------------------------------------- -You can log in to {kib} via SAML Single Sign-On by navigating directly to the {kib} URL. If you aren't authenticated, you are redirected to the Identity Provider for login. Most Identity Providers maintain a long-lived session. If you log in to a different application using the same Identity Provider in the same browser, you are automatically authenticated. An exception is if {es} or the Identity Provider is configured to force you to re-authenticate. This login scenario is called _Service Provider initiated login_. +You can log in to {kib} via SAML SSO by navigating directly to the {kib} URL. If you aren't authenticated, you are redirected to the Identity Provider for login. Most Identity Providers maintain a long-lived session. If you log in to a different application using the same Identity Provider in the same browser, you are automatically authenticated. An exception is if {es} or the Identity Provider is configured to force you to re-authenticate. This login scenario is called _Service Provider initiated login_. It's also possible to configure multiple SAML authentication providers at the same time. In this case, you will need to choose which provider to use for login at the Login Selector UI: @@ -176,7 +184,7 @@ xpack.security.authc.providers: [float] ===== SAML and basic authentication -You can also configure both SAML and basic authentication for the same {kib} instance. This might be the case for {kib} or {es} admins whose accounts aren't linked to the Single Sign-On users database: +You can also configure both SAML and basic authentication for the same {kib} instance. This might be the case for {kib} or {es} admins whose accounts aren't linked to the SSO users database: [source,yaml] -------------------------------------------------------------------------------- @@ -196,10 +204,11 @@ To support basic authentication for the applications like `curl` or when the `Au [[oidc]] ==== OpenID Connect single sign-on -Similar to SAML, authentication with OpenID Connect allows users to log in to {kib} using an OpenID Connect Provider such as Google, or Okta. OpenID Connect -should also be configured in {es}. For more details, see {ref}/oidc-guide.html[Configuring single sign-on to the {stack} using OpenID Connect]. +OpenID Connect (OIDC) authentication is part of single sign-on (SSO), a https://www.elastic.co/subscriptions[subscription feature]. Similar +to SAML, authentication with OIDC allows users to log in to {kib} using an OIDC Provider such as Google, or Okta. OIDC should also +be configured in {es}. For more details, see {ref}/oidc-guide.html[Configuring single sign-on to the {stack} using OpenID Connect]. -Enable the OpenID Connect authentication specifying which OpenID Connect realm in {es} should be used: +Enable OIDC authentication by specifying which OIDC realm in {es} to use: [source,yaml] -------------------------------------------------------------------------------- @@ -209,7 +218,7 @@ xpack.security.authc.providers: realm: oidc1 -------------------------------------------------------------------------------- -If you want to use Third Party initiated Single Sign-On, configure your OpenID Provider to use `/api/security/oidc/initiate_login` as `Initiate Login URI`. +To use third party initiated SSO, configure your OpenID Provider to use `/api/security/oidc/initiate_login` as `Initiate Login URI`. It's also possible to configure multiple OpenID Connect authentication providers at the same time. In this case, you need to choose which provider to use for login at the Login Selector UI: @@ -229,7 +238,7 @@ xpack.security.authc.providers: [float] ===== OpenID Connect and basic authentication -You can also configure both OpenID Connect and basic authentication for the same {kib} instance. This might be the case for {kib} or {es} admins whose accounts aren't linked to the Single Sign-On users database: +You can also configure both OpenID Connect and basic authentication for the same {kib} instance. This might be the case for {kib} or {es} admins whose accounts aren't linked to the SSO users database: [source,yaml] -------------------------------------------------------------------------------- @@ -254,7 +263,7 @@ The following sections apply both to <> and <> [float] ===== Access and refresh tokens -Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens +Once the user logs in to {kib} with SSO, either using SAML or OpenID Connect, {es} issues access and refresh tokens that {kib} encrypts and stores as a part of its own session. This way, the user isn't redirected to the Identity Provider for every request that requires authentication. It also means that the {kib} session depends on the <> settings, and the user is automatically logged @@ -281,7 +290,8 @@ all applications associated with the active provider session. [[kerberos]] ==== Kerberos single sign-on -As with the previous SSOs, make sure that you have configured {es} first accordingly. See {ref}/kerberos-realm.html[Kerberos authentication]. +Kerberos authentication is part of single sign-on (SSO), a https://www.elastic.co/subscriptions[subscription feature]. Make sure that +Kerberos is enabled and configured in {es} before setting it up in {kib}. See {ref}/kerberos-realm.html[Kerberos authentication]. Next, to enable Kerberos in {kib}, you will need to enable the Kerberos authentication provider in the `kibana.yml` configuration file, as follows: @@ -455,7 +465,7 @@ xpack.security.sameSiteCookies: "None" For more information about possible values and implications, refer to <>. For more information about iframe and cookies, refer to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe[iframe] and https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite[SameSite cookies]. -If you're embedding {kib} in a website that supports Single Sign-On with SAML, OpenID Connect, Kerberos, or PKI, it's highly advisable to configure {kib} as a part of the Single Sign-On setup. Operating in a single and properly configured security domain provides you with the most secure and seamless user experience. +If you're embedding {kib} in a website that supports single sign-on (SSO) with SAML, OpenID Connect, Kerberos, or PKI, it's highly advisable to configure {kib} as a part of the SSO setup. Operating in a single and properly configured security domain provides you with the most secure and seamless user experience. If you have multiple authentication providers enabled, and you want to automatically log in anonymous users when embedding anything other than dashboards and visualizations, then you will need to add the `auth_provider_hint=` query string parameter to the {kib} URL that you're embedding. diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 523a90bdf07ce4..df4ad4e2b89b08 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -63,11 +63,13 @@ With field-level security (FLS), you can instruct {es} to grant or deny access t .. `read` and `view_index_metadata` in the **Privileges** field. [role="screenshot"] -image::user/security/images/create-role-index-example.png[Create role with index privileges] +image::security/images/create-role-index-example.png[Create role with index privileges] [[index_privilege_dls_example]] ===== Example: Grant read access to specific documents in indices that match the `filebeat-*` pattern +{ref}/document-level-security.html[Document-level security] is a https://www.elastic.co/subscriptions[subscription feature]. + . Go to **Stack Management > Roles**, and then click **Create role**. . In **Index privileges**, enter: .. `filebeat-*` in the **Indices** field. @@ -87,7 +89,7 @@ image::user/security/images/create-role-index-example.png[Create role with index NOTE: {kib} automatically surrounds your DLS query with a `query` block, so you don't have to provide your own. [role="screenshot"] -image::user/security/images/create-role-dls-example.png[Create role with DLS index privileges] +image::security/images/create-role-dls-example.png[Create role with DLS index privileges] [[adding_kibana_privileges]] ==== {kib} privileges @@ -95,9 +97,9 @@ image::user/security/images/create-role-dls-example.png[Create role with DLS ind To assign {kib} privileges to the role, click **Add {kib} privilege** in the {kib} section. [role="screenshot"] -image::user/security/images/add-space-privileges.png[Add {kib} privileges] +image::spaces/images/spaces-roles.png[Add {kib} privileges] -Open the **Spaces** selection control to specify whether to grant the role access to all spaces *** Global (all spaces)** or one or more individual spaces. If you select *** Global (all spaces)**, you can’t select individual spaces until you clear your selection. +Open the **Spaces** selection control to specify whether to grant the role access to all spaces **All Spaces** or one or more individual spaces. If you select **All Spaces**, you can’t select individual spaces until you clear your selection. Use the **Privilege** menu to grant access to features. The default is **Custom**, which you can use to grant access to individual features. Otherwise, you can grant read and write access to all current and future features by selecting **All**, or grant read access to all current and future features by selecting **Read**. @@ -111,7 +113,7 @@ To apply your changes, click **Add {kib} privilege**. The privilege shows up und [role="screenshot"] -image::user/security/images/create-space-privilege.png[Add {kib} privilege] +image::security/images/create-space-privilege.png[Add {kib} privilege] ==== Feature availability @@ -139,9 +141,9 @@ Features are available to users when their roles grant access to the features, * ==== Assigning different privileges to different spaces -Using the same role, it’s possible to assign different privileges to different spaces. After you’ve added privileges, click **Add {kib} privilege**. If you’ve already added privileges for either *** Global (all spaces)** or an individual space, you will not be able to select these in the **Spaces** selection control. +Using the same role, it’s possible to assign different privileges to different spaces. After you’ve added privileges, click **Add {kib} privilege**. If you’ve already added privileges for either **All Spaces** or an individual space, you will not be able to select these in the **Spaces** selection control. -Additionally, if you’ve already assigned privileges at *** Global (all spaces)**, you are only able to assign additional privileges to individual spaces. Similar to the behavior of multiple roles granting the union of all privileges, {kib} privileges are also a union. If you’ve already granted the user the **All** privilege at *** Global (all spaces)**, you’re not able to restrict the role to only the **Read** privilege at an individual space. +Additionally, if you’ve already assigned privileges at **All Spaces**, you are only able to assign additional privileges to individual spaces. Similar to the behavior of multiple roles granting the union of all privileges, {kib} privileges are also a union. If you’ve already granted the user the **All** privilege at **All Spaces**, you’re not able to restrict the role to only the **Read** privilege at an individual space. ==== Privilege summary @@ -149,7 +151,7 @@ Additionally, if you’ve already assigned privileges at *** Global (all spaces) To view a summary of the privileges granted, click **View privilege summary**. [role="screenshot"] -image::user/security/images/view-privilege-summary.png[View privilege summary] +image::security/images/view-privilege-summary.png[View privilege summary] ==== Example 1: Grant all access to Dashboard at an individual space @@ -160,7 +162,7 @@ image::user/security/images/view-privilege-summary.png[View privilege summary] . Click **Add {kib} privilege**. [role="screenshot"] -image::user/security/images/privilege-example-1.png[Privilege example 1] +image::security/images/privilege-example-1.png[Privilege example 1] ==== Example 2: Grant all access to one space and read access to another @@ -173,12 +175,12 @@ image::user/security/images/privilege-example-1.png[Privilege example 1] . Click **Add {kib} privilege**. [role="screenshot"] -image::user/security/images/privilege-example-2.png[Privilege example 2] +image::security/images/privilege-example-2.png[Privilege example 2] ==== Example 3: Grant read access to all spaces and write access to an individual space . Click **Add {kib} privilege**. -. For **Spaces**, select *** Global (all spaces)**. +. For **Spaces**, select **All Spaces**. . For **Privilege**, select **Read**. . Click **Add {kib} privilege**. . For **Spaces**, select the individual space. @@ -186,4 +188,4 @@ image::user/security/images/privilege-example-2.png[Privilege example 2] . Click **Add {kib} privilege**. [role="screenshot"] -image::user/security/images/privilege-example-3.png[Privilege example 3] +image::security/images/privilege-example-3.png[Privilege example 3] diff --git a/docs/user/security/authorization/kibana-privileges.asciidoc b/docs/user/security/authorization/kibana-privileges.asciidoc index bbc8ec226fa7c4..581210bb9d3932 100644 --- a/docs/user/security/authorization/kibana-privileges.asciidoc +++ b/docs/user/security/authorization/kibana-privileges.asciidoc @@ -14,7 +14,7 @@ Assigning a base privilege grants access to all {kib} features, such as *Discove From the role management screen: [role="screenshot"] -image::user/security/images/assign_base_privilege.png[Assign base privilege] +image::security/images/assign-base-privilege.png[Assign base privilege] From the <>: [source,js] @@ -45,13 +45,13 @@ Assigning a feature privilege grants access to a specific feature. ===== Sub-feature privileges Some features allow for finer access control than the `all` and `read` privileges. -This additional level of control is available in the Gold subscription level and higher. +This additional level of control is a https://www.elastic.co/subscriptions[subscription feature]. ===== Assigning feature privileges From the role management screen: [role="screenshot"] -image::user/security/images/assign_feature_privilege.png[Assign feature privilege] +image::security/images/assign-subfeature-privilege.png[Assign feature privilege] From the <>: [source,js] diff --git a/docs/user/security/encryption-keys/index.asciidoc b/docs/user/security/encryption-keys/index.asciidoc index 58c0c0bb775caa..ff35070c2c9616 100644 --- a/docs/user/security/encryption-keys/index.asciidoc +++ b/docs/user/security/encryption-keys/index.asciidoc @@ -1,5 +1,5 @@ [[kibana-encryption-keys]] -=== Set up encryptions keys to protect sensitive information +=== Set up encryption keys to protect sensitive information The `kibana-encryption-keys` command helps you set up encryption keys that {kib} uses to protect sensitive information. @@ -18,7 +18,7 @@ bin/kibana-encryption-keys generate === Description {kib} uses encryption keys in several areas, ranging from encrypting data -in {kib} associated indices to storing session information. By defining these +in {kib} associated indices to storing session information. By defining these encryption keys in your configuration, you'll ensure consistent operations across restarts. diff --git a/docs/user/security/images/access-agreement.png b/docs/user/security/images/access-agreement.png index ecb6122875cb85..b7b762a88e85c3 100644 Binary files a/docs/user/security/images/access-agreement.png and b/docs/user/security/images/access-agreement.png differ diff --git a/docs/user/security/images/add-space-privileges.png b/docs/user/security/images/add-space-privileges.png deleted file mode 100644 index d2fcbe76c1a061..00000000000000 Binary files a/docs/user/security/images/add-space-privileges.png and /dev/null differ diff --git a/docs/user/security/images/assign-base-privilege.png b/docs/user/security/images/assign-base-privilege.png new file mode 100644 index 00000000000000..17be4b9ef2bf15 Binary files /dev/null and b/docs/user/security/images/assign-base-privilege.png differ diff --git a/docs/user/security/images/assign-subfeature-privilege.png b/docs/user/security/images/assign-subfeature-privilege.png new file mode 100644 index 00000000000000..c83cd6b1651574 Binary files /dev/null and b/docs/user/security/images/assign-subfeature-privilege.png differ diff --git a/docs/user/security/images/assign_base_privilege.png b/docs/user/security/images/assign_base_privilege.png deleted file mode 100644 index 93bed0de05555a..00000000000000 Binary files a/docs/user/security/images/assign_base_privilege.png and /dev/null differ diff --git a/docs/user/security/images/assign_feature_privilege.png b/docs/user/security/images/assign_feature_privilege.png deleted file mode 100644 index d42ec208325a33..00000000000000 Binary files a/docs/user/security/images/assign_feature_privilege.png and /dev/null differ diff --git a/docs/user/security/images/create-role-dls-example.png b/docs/user/security/images/create-role-dls-example.png index b0893502448b87..02b91ccf8820e9 100644 Binary files a/docs/user/security/images/create-role-dls-example.png and b/docs/user/security/images/create-role-dls-example.png differ diff --git a/docs/user/security/images/create-role-index-example.png b/docs/user/security/images/create-role-index-example.png index 200d01e0d56c59..8f95a741942748 100644 Binary files a/docs/user/security/images/create-role-index-example.png and b/docs/user/security/images/create-role-index-example.png differ diff --git a/docs/user/security/images/create-space-privilege.png b/docs/user/security/images/create-space-privilege.png old mode 100755 new mode 100644 index 2e6cc299bfc546..093a3391220843 Binary files a/docs/user/security/images/create-space-privilege.png and b/docs/user/security/images/create-space-privilege.png differ diff --git a/docs/user/security/images/kibana-login.png b/docs/user/security/images/kibana-login.png index 813f2c703908d8..cd9df9f5638795 100644 Binary files a/docs/user/security/images/kibana-login.png and b/docs/user/security/images/kibana-login.png differ diff --git a/docs/user/security/images/mutual-tls-role-mapping.png b/docs/user/security/images/mutual-tls-role-mapping.png index e870efe5d5b746..42ba56a2cac757 100644 Binary files a/docs/user/security/images/mutual-tls-role-mapping.png and b/docs/user/security/images/mutual-tls-role-mapping.png differ diff --git a/docs/user/security/images/privilege-example-1.png b/docs/user/security/images/privilege-example-1.png index b8fb4d15b8f77c..59e71e3cb3b241 100644 Binary files a/docs/user/security/images/privilege-example-1.png and b/docs/user/security/images/privilege-example-1.png differ diff --git a/docs/user/security/images/privilege-example-2.png b/docs/user/security/images/privilege-example-2.png old mode 100755 new mode 100644 index 2530ca5da36e10..36b26b131a3a2c Binary files a/docs/user/security/images/privilege-example-2.png and b/docs/user/security/images/privilege-example-2.png differ diff --git a/docs/user/security/images/privilege-example-3.png b/docs/user/security/images/privilege-example-3.png old mode 100755 new mode 100644 index c2e5db3a18e201..a0004905023f07 Binary files a/docs/user/security/images/privilege-example-3.png and b/docs/user/security/images/privilege-example-3.png differ diff --git a/docs/user/security/images/tutorial-secure-access-example-1-role.png b/docs/user/security/images/tutorial-secure-access-example-1-role.png index 53540da7170ea6..a27b56315e61cf 100644 Binary files a/docs/user/security/images/tutorial-secure-access-example-1-role.png and b/docs/user/security/images/tutorial-secure-access-example-1-role.png differ diff --git a/docs/user/security/images/tutorial-secure-access-example-1-test.png b/docs/user/security/images/tutorial-secure-access-example-1-test.png index 305b97017a9d8d..2ff4fb4b09c919 100644 Binary files a/docs/user/security/images/tutorial-secure-access-example-1-test.png and b/docs/user/security/images/tutorial-secure-access-example-1-test.png differ diff --git a/docs/user/security/images/view-privilege-summary.png b/docs/user/security/images/view-privilege-summary.png index 7d2f3018d7de9e..9e0e0fcc4f80ba 100644 Binary files a/docs/user/security/images/view-privilege-summary.png and b/docs/user/security/images/view-privilege-summary.png differ diff --git a/docs/user/security/role-mappings/images/role-mappings-create-step-1.png b/docs/user/security/role-mappings/images/role-mappings-create-step-1.png index 76aee8fd85b667..06ee1f3bc405d3 100644 Binary files a/docs/user/security/role-mappings/images/role-mappings-create-step-1.png and b/docs/user/security/role-mappings/images/role-mappings-create-step-1.png differ diff --git a/docs/user/security/role-mappings/images/role-mappings-create-step-2.gif b/docs/user/security/role-mappings/images/role-mappings-create-step-2.gif index c4a982d96c3651..766158a96bf1fb 100644 Binary files a/docs/user/security/role-mappings/images/role-mappings-create-step-2.gif and b/docs/user/security/role-mappings/images/role-mappings-create-step-2.gif differ diff --git a/docs/user/security/role-mappings/images/role-mappings-grid.png b/docs/user/security/role-mappings/images/role-mappings-grid.png index f33cd7a3652b2c..8044096e154892 100644 Binary files a/docs/user/security/role-mappings/images/role-mappings-grid.png and b/docs/user/security/role-mappings/images/role-mappings-grid.png differ diff --git a/docs/user/security/role-mappings/index.asciidoc b/docs/user/security/role-mappings/index.asciidoc index ca3ca9a686892a..df4ded4321c13b 100644 --- a/docs/user/security/role-mappings/index.asciidoc +++ b/docs/user/security/role-mappings/index.asciidoc @@ -2,11 +2,10 @@ [[role-mappings]] === Role mappings -Role mappings allow you to describe which roles to assign to your users -using a set of rules. Role mappings are required when authenticating via -an external identity provider, such as Active Directory, Kerberos, PKI, OIDC, -or SAML. +Role mappings are part of single sign-on (SSO), a https://www.elastic.co/subscriptions[subscription feature]. This feature allows you to +describe which roles to assign to your users using a set of rules. +Role mappings are required when authenticating via an external identity provider, such as Active Directory, Kerberos, PKI, OIDC, or SAML. Role mappings have no effect for users inside the `native` or `file` realms. To manage your role mappings, open the main menu, then click *Stack Management > Role Mappings*. @@ -17,7 +16,7 @@ With *Role mappings*, you can: * Create/Edit/Delete role mappings [role="screenshot"] -image:user/security/role-mappings/images/role-mappings-grid.png["Role mappings"] +image:images/role-mappings-grid.png["Role mappings"] [float] ==== Required permissions @@ -46,12 +45,12 @@ starts with `sls_`, *or* belongs to the `executive` group. First, we give the role mapping a name, and assign the `sales` role: [role="screenshot"] -image:user/security/role-mappings/images/role-mappings-create-step-1.png["Create role mapping, step 1"] +image:images/role-mappings-create-step-1.png["Create role mapping, step 1"] Next, we define the two rules, making sure to set the group to *Any are true*: [role="screenshot"] -image:user/security/role-mappings/images/role-mappings-create-step-2.gif["Create role mapping, step 2"] +image:images/role-mappings-create-step-2.gif["Create role mapping, step 2"] Click *Save role mapping* once you're finished. diff --git a/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc b/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc index 63fd2799c90cf8..57ecb1705e0074 100644 --- a/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc +++ b/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc @@ -109,7 +109,7 @@ This role mapping will assign the `kibana_system` role to any user that matches certificate's DN attribute: [role="screenshot"] -image:user/security/images/mutual-tls-role-mapping.png["Role mapping for the {kib} client certificate"] +image:security/images/mutual-tls-role-mapping.png["Role mapping for the {kib} client certificate"] For more information, see <>. -- diff --git a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc index 199f138347fa02..f5b799d4441022 100644 --- a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc +++ b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc @@ -61,7 +61,7 @@ Create a Marketing space for your marketing analysts to use. If you’ve followed the example above, you should end up with a space that looks like this: + [role="screenshot"] -image::user/security/images/tutorial-secure-access-example-1-space.png[Create space UI] +image::security/images/tutorial-secure-access-example-1-space.png[Create space UI] [float] @@ -93,7 +93,7 @@ TIP: You can add multiple patterns of indices, and grant different access levels If you’ve followed the example above, you should end up with a role that looks like this: + [role="screenshot"] -image::user/security/images/tutorial-secure-access-example-1-role.png[Create role UI] +image::security/images/tutorial-secure-access-example-1-role.png[Create role UI] [float] @@ -108,7 +108,7 @@ Now that you created a role, create a user account. . Click *Create user*. [role="screenshot"] -image::user/security/images/tutorial-secure-access-example-1-user.png[Create user UI] +image::security/images/tutorial-secure-access-example-1-user.png[Create user UI] [float] ==== Verify @@ -121,7 +121,7 @@ Verify that the user and role are working correctly. You’re taken into the `Marketing` space, and the main navigation shows only the *Dashboard* application. + [role="screenshot"] -image::user/security/images/tutorial-secure-access-example-1-test.png[Verifying access to dashboards] +image::security/images/tutorial-secure-access-example-1-test.png[Verifying access to dashboards] [float] 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/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/index.ts b/examples/preboot_example/public/config.ts similarity index 86% rename from src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/index.ts rename to examples/preboot_example/public/config.ts index 4f70f9d30b74bb..fc91296ce37249 100644 --- a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/index.ts +++ b/examples/preboot_example/public/config.ts @@ -6,4 +6,6 @@ * Side Public License, v 1. */ -export { strings as revealImage } from './reveal_image'; +export interface ConfigType { + token?: string; +} diff --git a/examples/preboot_example/public/index.ts b/examples/preboot_example/public/index.ts new file mode 100644 index 00000000000000..7859758c274f3e --- /dev/null +++ b/examples/preboot_example/public/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/public'; +import { PrebootExamplePlugin } from './plugin'; + +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/examples/preboot_example/server/config.ts b/examples/preboot_example/server/config.ts new file mode 100644 index 00000000000000..db39f985bca30e --- /dev/null +++ b/examples/preboot_example/server/config.ts @@ -0,0 +1,18 @@ +/* + * 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 { 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/jest.config.integration.js b/jest.config.integration.js index 8ff142714eebf9..e2b2afaa715ee2 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -6,24 +6,8 @@ * Side Public License, v 1. */ -const preset = require('@kbn/test/jest-preset'); - module.exports = { - preset: '@kbn/test', + preset: '@kbn/test/jest_integration', rootDir: '.', roots: ['/src', '/packages'], - testMatch: ['**/integration_tests**/*.test.{js,mjs,ts,tsx}'], - testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( - (pattern) => !pattern.includes('integration_tests') - ), - setupFilesAfterEnv: [ - '/node_modules/@kbn/test/target_node/jest/setup/after_env.integration.js', - ], - reporters: [ - 'default', - ['@kbn/test/target_node/jest/junit_reporter', { reportName: 'Jest Integration Tests' }], - ], - coverageReporters: !!process.env.CI - ? [['json', { file: 'jest-integration.json' }]] - : ['html', 'text'], }; diff --git a/package.json b/package.json index 5cf72e2110982f..85343fd8023d0e 100644 --- a/package.json +++ b/package.json @@ -83,10 +83,8 @@ "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", "**/pdfkit/crypto-js": "4.0.0", - "**/prismjs": "1.24.0", "**/react-syntax-highlighter": "^15.3.1", "**/react-syntax-highlighter/**/highlight.js": "^10.4.1", - "**/refractor": "^3.3.1", "**/request": "^2.88.2", "**/trim": "1.0.1", "**/typescript": "4.1.3", @@ -99,11 +97,11 @@ "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", - "@elastic/eui": "34.5.2", + "@elastic/eui": "35.0.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/maki": "6.3.0", @@ -155,6 +153,7 @@ "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils", "@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools", "@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository", + "@kbn/typed-react-router-config": "link:bazel-bin/packages/kbn-typed-react-router-config", "@kbn/std": "link:bazel-bin/packages/kbn-std", "@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath", "@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework", @@ -179,6 +178,7 @@ "@turf/distance": "6.0.1", "@turf/helpers": "6.0.1", "@turf/length": "^6.0.2", + "@types/react-router-config": "^5.0.2", "@types/redux-logger": "^3.0.8", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", @@ -358,6 +358,7 @@ "react-resize-detector": "^4.2.0", "react-reverse-portal": "^1.0.4", "react-router": "^5.2.0", + "react-router-config": "^5.1.1", "react-router-dom": "^5.2.0", "react-router-redux": "^4.0.8", "react-shortcuts": "^2.0.0", @@ -448,7 +449,7 @@ "@babel/traverse": "^7.12.12", "@babel/types": "^7.12.12", "@bazel/ibazel": "^0.15.10", - "@bazel/typescript": "^3.6.0", + "@bazel/typescript": "^3.7.0", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.6.0", "@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index de7a27fd512769..938afdc205a440 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -56,6 +56,7 @@ filegroup( "//packages/kbn-test:build", "//packages/kbn-test-subj-selector:build", "//packages/kbn-tinymath:build", + "//packages/kbn-typed-react-router-config:build", "//packages/kbn-ui-framework:build", "//packages/kbn-ui-shared-deps:build", "//packages/kbn-utility-types:build", 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-io-ts-utils/src/deep_exact_rt/index.test.ts b/packages/kbn-io-ts-utils/src/deep_exact_rt/index.test.ts new file mode 100644 index 00000000000000..fe09fb442799c5 --- /dev/null +++ b/packages/kbn-io-ts-utils/src/deep_exact_rt/index.test.ts @@ -0,0 +1,73 @@ +/* + * 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 * as t from 'io-ts'; +import { deepExactRt } from '.'; +import { mergeRt } from '../merge_rt'; + +describe('deepExactRt', () => { + it('recursively wraps partial/interface types in t.exact', () => { + const a = t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.type({ + foo: t.string, + }), + }); + + const b = t.type({ + path: t.type({ + transactionType: t.string, + }), + }); + + const merged = mergeRt(a, b); + + expect( + deepExactRt(a).decode({ + path: { + serviceName: '', + transactionType: '', + }, + query: { + foo: '', + bar: '', + }, + // @ts-ignore + }).right + ).toEqual({ path: { serviceName: '' }, query: { foo: '' } }); + + expect( + deepExactRt(b).decode({ + path: { + serviceName: '', + transactionType: '', + }, + query: { + foo: '', + bar: '', + }, + // @ts-ignore + }).right + ).toEqual({ path: { transactionType: '' } }); + + expect( + deepExactRt(merged).decode({ + path: { + serviceName: '', + transactionType: '', + }, + query: { + foo: '', + bar: '', + }, + // @ts-ignore + }).right + ).toEqual({ path: { serviceName: '', transactionType: '' }, query: { foo: '' } }); + }); +}); diff --git a/packages/kbn-io-ts-utils/src/deep_exact_rt/index.ts b/packages/kbn-io-ts-utils/src/deep_exact_rt/index.ts new file mode 100644 index 00000000000000..8ebb9bbdd52f91 --- /dev/null +++ b/packages/kbn-io-ts-utils/src/deep_exact_rt/index.ts @@ -0,0 +1,45 @@ +/* + * 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 * as t from 'io-ts'; +import { mapValues } from 'lodash'; +import { mergeRt } from '../merge_rt'; +import { isParsableType, ParseableType } from '../parseable_types'; + +export function deepExactRt | ParseableType>(type: T): T; + +export function deepExactRt(type: t.Type | ParseableType) { + if (!isParsableType(type)) { + return type; + } + + switch (type._tag) { + case 'ArrayType': + return t.array(deepExactRt(type.type)); + + case 'DictionaryType': + return t.dictionary(type.domain, deepExactRt(type.codomain)); + + case 'InterfaceType': + return t.exact(t.interface(mapValues(type.props, deepExactRt))); + + case 'PartialType': + return t.exact(t.partial(mapValues(type.props, deepExactRt))); + + case 'IntersectionType': + return t.intersection(type.types.map(deepExactRt) as any); + + case 'UnionType': + return t.union(type.types.map(deepExactRt) as any); + + case 'MergeType': + return mergeRt(deepExactRt(type.types[0]), deepExactRt(type.types[1])); + + default: + return type; + } +} diff --git a/packages/kbn-io-ts-utils/src/parseable_types/index.ts b/packages/kbn-io-ts-utils/src/parseable_types/index.ts new file mode 100644 index 00000000000000..089717ad8891bf --- /dev/null +++ b/packages/kbn-io-ts-utils/src/parseable_types/index.ts @@ -0,0 +1,39 @@ +/* + * 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 * as t from 'io-ts'; +import { MergeType } from '../merge_rt'; + +export type ParseableType = + | t.StringType + | t.NumberType + | t.BooleanType + | t.ArrayType + | t.RecordC + | t.DictionaryType + | t.InterfaceType + | t.PartialType + | t.UnionType + | t.IntersectionType + | MergeType; + +const parseableTags = [ + 'StringType', + 'NumberType', + 'BooleanType', + 'ArrayType', + 'DictionaryType', + 'InterfaceType', + 'PartialType', + 'UnionType', + 'IntersectionType', + 'MergeType', +]; + +export const isParsableType = (type: t.Type | ParseableType): type is ParseableType => { + return '_tag' in type && parseableTags.includes(type._tag); +}; diff --git a/packages/kbn-io-ts-utils/src/to_json_schema/index.ts b/packages/kbn-io-ts-utils/src/to_json_schema/index.ts index fc196a7c3123eb..702c0150d07f74 100644 --- a/packages/kbn-io-ts-utils/src/to_json_schema/index.ts +++ b/packages/kbn-io-ts-utils/src/to_json_schema/index.ts @@ -7,35 +7,7 @@ */ import * as t from 'io-ts'; import { mapValues } from 'lodash'; - -type JSONSchemableValueType = - | t.StringType - | t.NumberType - | t.BooleanType - | t.ArrayType - | t.RecordC - | t.DictionaryType - | t.InterfaceType - | t.PartialType - | t.UnionType - | t.IntersectionType; - -const tags = [ - 'StringType', - 'NumberType', - 'BooleanType', - 'ArrayType', - 'DictionaryType', - 'InterfaceType', - 'PartialType', - 'UnionType', - 'IntersectionType', -]; - -const isSchemableValueType = (type: t.Mixed): type is JSONSchemableValueType => { - // @ts-ignore - return tags.includes(type._tag); -}; +import { isParsableType } from '../parseable_types'; interface JSONSchemaObject { type: 'object'; @@ -74,7 +46,7 @@ type JSONSchema = | JSONSchemaAnyOf; export const toJsonSchema = (type: t.Mixed): JSONSchema => { - if (isSchemableValueType(type)) { + if (isParsableType(type)) { switch (type._tag) { case 'ArrayType': return { type: 'array', items: toJsonSchema(type.type) }; diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts index dc5f1ed95205cf..3d13d76743dbc4 100644 --- a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts @@ -35,6 +35,11 @@ export class DiagnosticsAdapter { return; } + // Reset the model markers if an empty string is provided on change + if (model.getValue().trim() === '') { + return monaco.editor.setModelMarkers(model, ID, []); + } + // Every time a new change is made, wait 500ms before validating clearTimeout(handle); handle = setTimeout(() => this.validate(model.uri), 500); @@ -42,8 +47,11 @@ export class DiagnosticsAdapter { model.onDidChangeLanguage(({ newLanguage }) => { // Reset the model markers if the language ID has changed and is no longer "painless" + // Otherwise, re-validate if (newLanguage !== ID) { return monaco.editor.setModelMarkers(model, ID, []); + } else { + this.validate(model.uri); } }); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 4524cbe8912cf6..2f6765cd57b9e1 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -107,9 +107,10 @@ pageLoadAssetSize: dataVisualizer: 27530 banners: 17946 mapsEms: 26072 - timelines: 230410 + timelines: 251886 screenshotMode: 17856 visTypePie: 35583 expressionRevealImage: 25675 cases: 144442 + expressionError: 22127 userSetup: 18532 diff --git a/packages/kbn-rule-data-utils/package.json b/packages/kbn-rule-data-utils/package.json index 6f0b8439ec8915..42223e51ec2d6d 100644 --- a/packages/kbn-rule-data-utils/package.json +++ b/packages/kbn-rule-data-utils/package.json @@ -4,10 +4,5 @@ "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - } + "private": true } diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 6c45403fc0a136..3cabb307b1654d 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -8,7 +8,7 @@ import { ValuesType } from 'utility-types'; -const ALERT_NAMESPACE = 'kibana.rac.alert'; +const ALERT_NAMESPACE = 'kibana.rac.alert' as const; const TIMESTAMP = '@timestamp' as const; const EVENT_KIND = 'event.kind' as const; @@ -28,6 +28,7 @@ const ALERT_DURATION = `${ALERT_NAMESPACE}.duration.us` as const; const ALERT_SEVERITY_LEVEL = `${ALERT_NAMESPACE}.severity.level` as const; const ALERT_SEVERITY_VALUE = `${ALERT_NAMESPACE}.severity.value` as const; const ALERT_STATUS = `${ALERT_NAMESPACE}.status` as const; +const SPACE_IDS = 'kibana.space_ids' as const; const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; const ALERT_EVALUATION_VALUE = `${ALERT_NAMESPACE}.evaluation.value` as const; @@ -52,6 +53,7 @@ const fields = { ALERT_STATUS, ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, + SPACE_IDS, }; export { @@ -75,6 +77,7 @@ export { ALERT_STATUS, ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, + SPACE_IDS, }; export type TechnicalRuleDataFieldName = ValuesType; diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index 28c42a4c476846..432778f888e7a7 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -28,6 +28,7 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "jest/package.json", "jest-preset.js", + "jest_integration/jest-preset.js", "jest.config.js", "README.md", "package.json", diff --git a/packages/kbn-test/jest_integration/jest-preset.js b/packages/kbn-test/jest_integration/jest-preset.js new file mode 100644 index 00000000000000..7504dec9e7a20f --- /dev/null +++ b/packages/kbn-test/jest_integration/jest-preset.js @@ -0,0 +1,28 @@ +/* + * 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 preset = require('../jest-preset'); + +module.exports = { + ...preset, + testMatch: ['**/integration_tests**/*.test.{js,mjs,ts,tsx}'], + testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( + (pattern) => !pattern.includes('integration_tests') + ), + setupFilesAfterEnv: [ + '/node_modules/@kbn/test/target_node/jest/setup/after_env.integration.js', + '/node_modules/@kbn/test/target_node/jest/setup/mocks.js', + ], + reporters: [ + 'default', + ['@kbn/test/target_node/jest/junit_reporter', { reportName: 'Jest Integration Tests' }], + ], + coverageReporters: !!process.env.CI + ? [['json', { file: 'jest-integration.json' }]] + : ['html', 'text'], +}; diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel new file mode 100644 index 00000000000000..90f1acf43d3e7d --- /dev/null +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -0,0 +1,113 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-typed-react-router-config" +PKG_REQUIRE_NAME = "@kbn/typed-react-router-config" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + ] +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +SRC_DEPS = [ + "@npm//tslib", + "@npm//utility-types", + "@npm//io-ts", + "@npm//query-string", + "@npm//react-router-config", + "@npm//react-router-dom", + "//packages/kbn-io-ts-utils", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react-router-config", + "@npm//@types/react-router-dom", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_dir = "target_types", + declaration_map = True, + incremental = True, + out_dir = "target_node", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +ts_project( + name = "tsc_browser", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = False, + incremental = True, + out_dir = "target_web", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig_browser", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":tsc", ":tsc_browser"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts b/packages/kbn-typed-react-router-config/jest.config.js similarity index 73% rename from src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts rename to packages/kbn-typed-react-router-config/jest.config.js index 09cd26c9e620b9..3a6b09c5677dba 100644 --- a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts +++ b/packages/kbn-typed-react-router-config/jest.config.js @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { errors as revealImage } from './dict/reveal_image'; - -export const getFunctionErrors = () => ({ - revealImage, -}); +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-typed-react-router-config'], +}; diff --git a/packages/kbn-typed-react-router-config/package.json b/packages/kbn-typed-react-router-config/package.json new file mode 100644 index 00000000000000..50c2e4b5d7e890 --- /dev/null +++ b/packages/kbn-typed-react-router-config/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/typed-react-router-config", + "main": "target_node/index.js", + "types": "target_types/index.d.ts", + "browser": "target_web/index.js", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true +} diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx new file mode 100644 index 00000000000000..97b6d16e3214e5 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -0,0 +1,245 @@ +/* + * 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 React from 'react'; +import * as t from 'io-ts'; +import { toNumberRt } from '@kbn/io-ts-utils/target/to_number_rt'; +import { createRouter } from './create_router'; +import { createMemoryHistory } from 'history'; +import { route } from './route'; + +describe('createRouter', () => { + const routes = route([ + { + path: '/', + element: <>, + children: [ + { + path: '/', + element: <>, + params: t.type({ + query: t.type({ + rangeFrom: t.string, + rangeTo: t.string, + }), + }), + children: [ + { + path: '/services', + element: <>, + params: t.type({ + query: t.type({ + transactionType: t.string, + }), + }), + }, + { + path: '/services/:serviceName', + element: <>, + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.type({ + transactionType: t.string, + environment: t.string, + }), + }), + }, + { + path: '/traces', + element: <>, + params: t.type({ + query: t.type({ + aggregationType: t.string, + }), + }), + }, + { + path: '/service-map', + element: <>, + params: t.type({ + query: t.type({ + maxNumNodes: t.string.pipe(toNumberRt as any), + }), + }), + }, + ], + }, + ], + }, + ] as const); + + let history = createMemoryHistory(); + const router = createRouter(routes); + + beforeEach(() => { + history = createMemoryHistory(); + }); + + describe('getParams', () => { + it('returns parameters for routes matching the path only', () => { + history.push('/services?rangeFrom=now-15m&rangeTo=now&transactionType=request'); + const topLevelParams = router.getParams('/', history.location); + + expect(topLevelParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + }, + }); + + history.push('/services?rangeFrom=now-15m&rangeTo=now&transactionType=request'); + + const inventoryParams = router.getParams('/services', history.location); + + expect(inventoryParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + transactionType: 'request', + }, + }); + + history.push('/traces?rangeFrom=now-15m&rangeTo=now&aggregationType=avg'); + + const topTracesParams = router.getParams('/traces', history.location); + + expect(topTracesParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + aggregationType: 'avg', + }, + }); + + history.push( + '/services/opbeans-java?rangeFrom=now-15m&rangeTo=now&environment=production&transactionType=request' + ); + + const serviceOverviewParams = router.getParams('/services/:serviceName', history.location); + + expect(serviceOverviewParams).toEqual({ + path: { + serviceName: 'opbeans-java', + }, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + environment: 'production', + transactionType: 'request', + }, + }); + }); + + it('decodes the path and query parameters based on the route type', () => { + history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3'); + const topServiceMapParams = router.getParams('/service-map', history.location); + + expect(topServiceMapParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + maxNumNodes: 3, + }, + }); + }); + + it('throws an error if the given path does not match any routes', () => { + expect(() => { + router.getParams('/service-map', history.location); + }).toThrowError('No matching route found for /service-map'); + }); + + it('does not throw an error if the given path does not match any routes but is marked as optional', () => { + expect(() => { + router.getParams('/service-map', history.location, true); + }).not.toThrowError(); + }); + }); + + describe('matchRoutes', () => { + it('returns only the routes matching the path', () => { + history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3'); + + expect(router.matchRoutes('/', history.location).length).toEqual(2); + expect(router.matchRoutes('/service-map', history.location).length).toEqual(3); + }); + + it('throws an error if the given path does not match any routes', () => { + history.push('/service-map?rangeFrom=now-15m&rangeTo=now&maxNumNodes=3'); + + expect(() => { + router.matchRoutes('/traces', history.location); + }).toThrowError('No matching route found for /traces'); + }); + }); + + describe('link', () => { + it('returns a link for the given route', () => { + const serviceOverviewLink = router.link('/services/:serviceName', { + path: { serviceName: 'opbeans-java' }, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + environment: 'production', + transactionType: 'request', + }, + }); + + expect(serviceOverviewLink).toEqual( + '/services/opbeans-java?environment=production&rangeFrom=now-15m&rangeTo=now&transactionType=request' + ); + + const servicesLink = router.link('/services', { + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + transactionType: 'request', + }, + }); + + expect(servicesLink).toEqual( + '/services?rangeFrom=now-15m&rangeTo=now&transactionType=request' + ); + + const serviceMapLink = router.link('/service-map', { + query: { + maxNumNodes: '3', + rangeFrom: 'now-15m', + rangeTo: 'now', + }, + }); + + expect(serviceMapLink).toEqual('/service-map?maxNumNodes=3&rangeFrom=now-15m&rangeTo=now'); + }); + + it('validates the parameters needed for the route', () => { + expect(() => { + router.link('/traces', { + query: { + rangeFrom: {}, + }, + } as any); + }).toThrowError(); + + expect(() => { + router.link('/service-map', { + query: { + maxNumNodes: 3, + rangeFrom: 'now-15m', + rangeTo: 'now', + }, + } as any); + }).toThrowError(); + }); + }); +}); diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts new file mode 100644 index 00000000000000..4a8b89560d5166 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -0,0 +1,165 @@ +/* + * 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 { isLeft } from 'fp-ts/lib/Either'; +import { Location } from 'history'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { + matchRoutes as matchRoutesConfig, + RouteConfig as ReactRouterConfig, +} from 'react-router-config'; +import qs from 'query-string'; +import { findLastIndex, merge, compact } from 'lodash'; +import { deepExactRt } from '@kbn/io-ts-utils/target/deep_exact_rt'; +import { mergeRt } from '@kbn/io-ts-utils/target/merge_rt'; +import { Route, Router } from './types'; + +export function createRouter(routes: TRoutes): Router { + const routesByReactRouterConfig = new Map(); + const reactRouterConfigsByRoute = new Map(); + + const reactRouterConfigs = routes.map((route) => toReactRouterConfigRoute(route)); + + function toReactRouterConfigRoute(route: Route, prefix: string = ''): ReactRouterConfig { + const path = `${prefix}${route.path}`.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/'; + const reactRouterConfig: ReactRouterConfig = { + component: () => route.element, + routes: + (route.children as Route[] | undefined)?.map((child) => + toReactRouterConfigRoute(child, path) + ) ?? [], + exact: !route.children?.length, + path, + }; + + routesByReactRouterConfig.set(reactRouterConfig, route); + reactRouterConfigsByRoute.set(route, reactRouterConfig); + + return reactRouterConfig; + } + + const matchRoutes = (...args: any[]) => { + let path: string = args[0]; + let location: Location = args[1]; + let optional: boolean = args[2]; + + if (args.length === 1) { + location = args[0] as Location; + path = location.pathname; + optional = args[1]; + } + + const greedy = path.endsWith('/*') || args.length === 1; + + if (!path) { + path = '/'; + } + + const matches = matchRoutesConfig(reactRouterConfigs, location.pathname); + + const matchIndex = greedy + ? matches.length - 1 + : findLastIndex(matches, (match) => match.route.path === path); + + if (matchIndex === -1) { + if (optional) { + return []; + } + throw new Error(`No matching route found for ${path}`); + } + + return matches.slice(0, matchIndex + 1).map((matchedRoute) => { + const route = routesByReactRouterConfig.get(matchedRoute.route); + + if (route?.params) { + const decoded = deepExactRt(route.params).decode({ + path: matchedRoute.match.params, + query: qs.parse(location.search), + }); + + if (isLeft(decoded)) { + throw new Error(PathReporter.report(decoded).join('\n')); + } + + return { + match: { + ...matchedRoute.match, + params: decoded.right, + }, + route, + }; + } + + return { + match: { + ...matchedRoute.match, + params: { + path: {}, + query: {}, + }, + }, + route, + }; + }); + }; + + const link = (path: string, ...args: any[]) => { + const params: { path?: Record; query?: Record } | undefined = args[0]; + + const paramsWithDefaults = merge({ path: {}, query: {} }, params); + + path = path + .split('/') + .map((part) => { + return part.startsWith(':') ? paramsWithDefaults.path[part.split(':')[1]] : part; + }) + .join('/'); + + const matches = matchRoutesConfig(reactRouterConfigs, path); + + if (!matches.length) { + throw new Error(`No matching route found for ${path}`); + } + + const validationType = mergeRt( + ...(compact( + matches.map((match) => { + return routesByReactRouterConfig.get(match.route)?.params; + }) + ) as [any, any]) + ); + + const validation = validationType.decode(paramsWithDefaults); + + if (isLeft(validation)) { + throw new Error(PathReporter.report(validation).join('\n')); + } + + return qs.stringifyUrl({ + url: path, + query: paramsWithDefaults.query, + }); + }; + + return { + link: (path, ...args) => { + return link(path, ...args); + }, + getParams: (...args: any[]) => { + const matches = matchRoutes(...args); + return matches.length + ? merge({ path: {}, query: {} }, ...matches.map((match) => match.match.params)) + : undefined; + }, + matchRoutes: (...args: any[]) => { + return matchRoutes(...args) as any; + }, + getRoutePath: (route) => { + return reactRouterConfigsByRoute.get(route)!.path as string; + }, + }; +} diff --git a/packages/kbn-typed-react-router-config/src/index.ts b/packages/kbn-typed-react-router-config/src/index.ts new file mode 100644 index 00000000000000..b58c70998901c3 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ +export * from './create_router'; +export * from './types'; +export * from './outlet'; +export * from './route'; +export * from './route_renderer'; +export * from './router_provider'; +export * from './unconst'; +export * from './use_current_route'; +export * from './use_match_routes'; +export * from './use_params'; +export * from './use_router'; +export * from './use_route_path'; diff --git a/packages/kbn-typed-react-router-config/src/outlet.tsx b/packages/kbn-typed-react-router-config/src/outlet.tsx new file mode 100644 index 00000000000000..696085489abee1 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/outlet.tsx @@ -0,0 +1,13 @@ +/* + * 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 { useCurrentRoute } from './use_current_route'; + +export function Outlet() { + const { element } = useCurrentRoute(); + return element; +} diff --git a/packages/kbn-typed-react-router-config/src/route.ts b/packages/kbn-typed-react-router-config/src/route.ts new file mode 100644 index 00000000000000..b9b228d1009e2f --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/route.ts @@ -0,0 +1,15 @@ +/* + * 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 { Route } from './types'; +import { Unconst, unconst } from './unconst'; + +export function route( + r: TRoute +): Unconst { + return unconst(r); +} diff --git a/packages/kbn-typed-react-router-config/src/route_renderer.tsx b/packages/kbn-typed-react-router-config/src/route_renderer.tsx new file mode 100644 index 00000000000000..e7a39aa7d5d165 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/route_renderer.tsx @@ -0,0 +1,27 @@ +/* + * 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 React from 'react'; +import { CurrentRouteContextProvider } from './use_current_route'; +import { RouteMatch } from './types'; +import { useMatchRoutes } from './use_match_routes'; + +export function RouteRenderer() { + const matches: RouteMatch[] = useMatchRoutes(); + + return matches + .concat() + .reverse() + .reduce((prev, match) => { + const { element } = match.route; + return ( + + {element} + + ); + }, <>); +} diff --git a/packages/kbn-typed-react-router-config/src/router_provider.tsx b/packages/kbn-typed-react-router-config/src/router_provider.tsx new file mode 100644 index 00000000000000..d2512ba8fe4265 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/router_provider.tsx @@ -0,0 +1,28 @@ +/* + * 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 { History } from 'history'; +import React from 'react'; +import { Router as ReactRouter } from 'react-router-dom'; +import { Route, Router } from './types'; +import { RouterContextProvider } from './use_router'; + +export function RouterProvider({ + children, + router, + history, +}: { + router: Router; + history: History; + children: React.ReactElement; +}) { + return ( + + {children} + + ); +} diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts new file mode 100644 index 00000000000000..f1609d87096206 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -0,0 +1,426 @@ +/* + * 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 { Location } from 'history'; +import * as t from 'io-ts'; +import { ReactElement } from 'react'; +import { RequiredKeys, ValuesType } from 'utility-types'; +// import { unconst } from '../unconst'; +import { NormalizePath } from './utils'; + +export type PathsOf = keyof MapRoutes & string; + +export interface RouteMatch { + route: TRoute; + match: { + isExact: boolean; + path: string; + url: string; + params: TRoute extends { + params: t.Type; + } + ? t.OutputOf + : {}; + }; +} + +type ToRouteMatch = TRoutes extends [] + ? [] + : TRoutes extends [Route] + ? [RouteMatch] + : TRoutes extends [Route, ...infer TTail] + ? TTail extends Route[] + ? [RouteMatch, ...ToRouteMatch] + : [] + : []; + +type UnwrapRouteMap = TRoute extends { + parents: Route[]; +} + ? ToRouteMatch<[...TRoute['parents'], Omit]> + : ToRouteMatch<[Omit]>; + +export type Match = MapRoutes extends { + [key in TPath]: Route; +} + ? UnwrapRouteMap[TPath]> + : []; + +interface PlainRoute { + path: string; + element: ReactElement; + children?: PlainRoute[]; + params?: t.Type; +} + +interface ReadonlyPlainRoute { + readonly path: string; + readonly element: ReactElement; + readonly children?: readonly ReadonlyPlainRoute[]; + readonly params?: t.Type; +} + +export type Route = PlainRoute | ReadonlyPlainRoute; + +interface DefaultOutput { + path: {}; + query: {}; +} + +type OutputOfRouteMatch = TRouteMatch extends { + route: { params: t.Type }; +} + ? t.OutputOf + : DefaultOutput; + +type OutputOfMatches = TRouteMatches extends [RouteMatch] + ? OutputOfRouteMatch + : TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches] + ? OutputOfRouteMatch & + (TNextRouteMatches extends RouteMatch[] ? OutputOfMatches : DefaultOutput) + : TRouteMatches extends RouteMatch[] + ? OutputOfRouteMatch> + : DefaultOutput; + +export type OutputOf> = OutputOfMatches< + Match +> & + DefaultOutput; + +type TypeOfRouteMatch = TRouteMatch extends { + route: { params: t.Type }; +} + ? t.TypeOf + : {}; + +type TypeOfMatches = TRouteMatches extends [RouteMatch] + ? TypeOfRouteMatch + : TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches] + ? TypeOfRouteMatch & + (TNextRouteMatches extends RouteMatch[] ? TypeOfMatches : {}) + : {}; + +export type TypeOf> = TypeOfMatches< + Match +>; + +export type TypeAsArgs = keyof TObject extends never + ? [] + : RequiredKeys extends never + ? [TObject] | [] + : [TObject]; + +export interface Router { + matchRoutes>( + path: TPath, + location: Location + ): Match; + matchRoutes(location: Location): Match>; + getParams>( + path: TPath, + location: Location + ): OutputOf; + getParams, TOptional extends boolean>( + path: TPath, + location: Location, + optional: TOptional + ): TOptional extends true ? OutputOf | undefined : OutputOf; + link>( + path: TPath, + ...args: TypeAsArgs> + ): string; + getRoutePath(route: Route): string; +} + +type AppendPath< + TPrefix extends string, + TPath extends string +> = NormalizePath<`${TPrefix}${NormalizePath<`/${TPath}`>}`>; + +type MaybeUnion, U extends Record> = Omit & + { + [key in keyof U]: key extends keyof T ? T[key] | U[key] : U[key]; + }; + +type MapRoute< + TRoute extends Route, + TPrefix extends string, + TParents extends Route[] = [] +> = TRoute extends Route + ? MaybeUnion< + { + [key in AppendPath]: TRoute & { parents: TParents }; + }, + TRoute extends { children: Route[] } + ? MaybeUnion< + MapRoutes< + TRoute['children'], + AppendPath, + [...TParents, TRoute] + >, + { + [key in AppendPath>]: ValuesType< + MapRoutes< + TRoute['children'], + AppendPath, + [...TParents, TRoute] + > + >; + } + > + : {} + > + : {}; + +type MapRoutes< + TRoutes, + TPrefix extends string = '', + TParents extends Route[] = [] +> = TRoutes extends [Route] + ? MapRoute + : TRoutes extends [Route, Route] + ? MapRoute & MapRoute + : TRoutes extends [Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute + : {}; + +// const element = null as any; + +// const routes = unconst([ +// { +// path: '/', +// element, +// children: [ +// { +// path: '/settings', +// element, +// children: [ +// { +// path: '/agent-configuration', +// element, +// }, +// { +// path: '/agent-configuration/create', +// element, +// params: t.partial({ +// query: t.partial({ +// pageStep: t.string, +// }), +// }), +// }, +// { +// path: '/agent-configuration/edit', +// element, +// params: t.partial({ +// query: t.partial({ +// pageStep: t.string, +// }), +// }), +// }, +// { +// path: '/apm-indices', +// element, +// }, +// { +// path: '/customize-ui', +// element, +// }, +// { +// path: '/schema', +// element, +// }, +// { +// path: '/anomaly-detection', +// element, +// }, +// { +// path: '/', +// element, +// }, +// ], +// }, +// { +// path: '/services/:serviceName', +// element, +// params: t.intersection([ +// t.type({ +// path: t.type({ +// serviceName: t.string, +// }), +// }), +// t.partial({ +// query: t.partial({ +// environment: t.string, +// rangeFrom: t.string, +// rangeTo: t.string, +// comparisonEnabled: t.string, +// comparisonType: t.string, +// latencyAggregationType: t.string, +// transactionType: t.string, +// kuery: t.string, +// }), +// }), +// ]), +// children: [ +// { +// path: '/overview', +// element, +// }, +// { +// path: '/transactions', +// element, +// }, +// { +// path: '/errors', +// element, +// children: [ +// { +// path: '/:groupId', +// element, +// params: t.type({ +// path: t.type({ +// groupId: t.string, +// }), +// }), +// }, +// { +// path: '/', +// element, +// params: t.partial({ +// query: t.partial({ +// sortDirection: t.string, +// sortField: t.string, +// pageSize: t.string, +// page: t.string, +// }), +// }), +// }, +// ], +// }, +// { +// path: '/foo', +// element, +// }, +// { +// path: '/bar', +// element, +// }, +// { +// path: '/baz', +// element, +// }, +// { +// path: '/', +// element, +// }, +// ], +// }, +// { +// path: '/', +// element, +// params: t.partial({ +// query: t.partial({ +// rangeFrom: t.string, +// rangeTo: t.string, +// }), +// }), +// children: [ +// { +// path: '/services', +// element, +// }, +// { +// path: '/traces', +// element, +// }, +// { +// path: '/service-map', +// element, +// }, +// { +// path: '/', +// element, +// }, +// ], +// }, +// ], +// }, +// ] as const); + +// type Routes = typeof routes; + +// type Mapped = keyof MapRoutes; + +// type Bar = ValuesType>['route']['path']; +// type Foo = OutputOf; + +// const { path }: Foo = {} as any; + +// function _useApmParams>(p: TPath): OutputOf { +// return {} as any; +// } + +// const params = _useApmParams('/*'); diff --git a/packages/kbn-typed-react-router-config/src/types/utils.ts b/packages/kbn-typed-react-router-config/src/types/utils.ts new file mode 100644 index 00000000000000..38b23c5118d0f6 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/types/utils.ts @@ -0,0 +1,31 @@ +/* + * 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 * as t from 'io-ts'; + +export type MaybeOutputOf = T extends t.Type ? [t.OutputOf] : []; +export type NormalizePath = T extends `//${infer TRest}` + ? NormalizePath<`/${TRest}`> + : T extends '/' + ? T + : T extends `${infer TRest}/` + ? TRest + : T; +export type DeeplyMutableRoutes = T extends React.ReactElement + ? T + : T extends t.Type + ? T + : T extends readonly [infer U] + ? [DeeplyMutableRoutes] + : T extends readonly [infer U, ...infer V] + ? [DeeplyMutableRoutes, ...DeeplyMutableRoutes] + : T extends Record + ? { + -readonly [key in keyof T]: DeeplyMutableRoutes; + } + : T; diff --git a/packages/kbn-typed-react-router-config/src/unconst.ts b/packages/kbn-typed-react-router-config/src/unconst.ts new file mode 100644 index 00000000000000..d10c8290e20e9a --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/unconst.ts @@ -0,0 +1,91 @@ +/* + * 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 * as t from 'io-ts'; +import { DeepReadonly } from 'utility-types'; + +export type MaybeConst = TObject extends [object] + ? [DeepReadonly | TObject] + : TObject extends [object, ...infer TTail] + ? [DeepReadonly | TObject, ...(TTail extends object[] ? MaybeConst : [])] + : TObject extends object[] + ? DeepReadonly + : TObject extends object + ? [DeepReadonly | TObject] + : []; + +export type Unconst = T extends React.ReactElement + ? React.ReactElement + : T extends t.Type + ? T + : T extends readonly [any] + ? [Unconst] + : T extends readonly [any, any] + ? [Unconst, Unconst] + : T extends readonly [any, any, any] + ? [Unconst, Unconst, Unconst] + : T extends readonly [any, any, any, any] + ? [Unconst, Unconst, Unconst, Unconst] + : T extends readonly [any, any, any, any, any] + ? [Unconst, Unconst, Unconst, Unconst, Unconst] + : T extends readonly [any, any, any, any, any, any] + ? [Unconst, Unconst, Unconst, Unconst, Unconst, Unconst] + : T extends readonly [any, any, any, any, any, any, any] + ? [ + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst + ] + : T extends readonly [any, any, any, any, any, any, any, any] + ? [ + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst + ] + : T extends readonly [any, any, any, any, any, any, any, any, any] + ? [ + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst + ] + : T extends readonly [any, any, any, any, any, any, any, any, any, any] + ? [ + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst, + Unconst + ] + : T extends readonly [infer U, ...infer V] + ? [Unconst, ...Unconst] + : T extends Record + ? { -readonly [key in keyof T]: Unconst } + : T; + +export function unconst(value: T): Unconst { + return value as Unconst; +} diff --git a/packages/kbn-typed-react-router-config/src/use_current_route.tsx b/packages/kbn-typed-react-router-config/src/use_current_route.tsx new file mode 100644 index 00000000000000..9227b119107b37 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/use_current_route.tsx @@ -0,0 +1,37 @@ +/* + * 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 React, { createContext, useContext } from 'react'; +import { RouteMatch } from './types'; + +const CurrentRouteContext = createContext< + { match: RouteMatch; element: React.ReactElement } | undefined +>(undefined); + +export const CurrentRouteContextProvider = ({ + match, + element, + children, +}: { + match: RouteMatch; + element: React.ReactElement; + children: React.ReactElement; +}) => { + return ( + + {children} + + ); +}; + +export const useCurrentRoute = () => { + const currentRoute = useContext(CurrentRouteContext); + if (!currentRoute) { + throw new Error('No match was found in context'); + } + return currentRoute; +}; diff --git a/packages/kbn-typed-react-router-config/src/use_match_routes.ts b/packages/kbn-typed-react-router-config/src/use_match_routes.ts new file mode 100644 index 00000000000000..b818ff06e9ae61 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/use_match_routes.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 { useLocation } from 'react-router-dom'; +import { RouteMatch } from './types'; +import { useRouter } from './use_router'; + +export function useMatchRoutes(path?: string): RouteMatch[] { + const router = useRouter(); + const location = useLocation(); + + return typeof path === 'undefined' + ? router.matchRoutes(location) + : router.matchRoutes(path as never, location); +} diff --git a/packages/kbn-typed-react-router-config/src/use_params.ts b/packages/kbn-typed-react-router-config/src/use_params.ts new file mode 100644 index 00000000000000..94a5cf401c5698 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/use_params.ts @@ -0,0 +1,17 @@ +/* + * 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 { useLocation } from 'react-router-dom'; +import { useRouter } from './use_router'; + +export function useParams(path: string, optional: boolean = false) { + const router = useRouter(); + const location = useLocation(); + + return router.getParams(path as never, location, optional); +} diff --git a/packages/kbn-typed-react-router-config/src/use_route_path.tsx b/packages/kbn-typed-react-router-config/src/use_route_path.tsx new file mode 100644 index 00000000000000..c77fc7d04b620c --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/use_route_path.tsx @@ -0,0 +1,21 @@ +/* + * 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 { last } from 'lodash'; +import { useMatchRoutes } from './use_match_routes'; +import { useRouter } from './use_router'; + +export function useRoutePath() { + const lastRouteMatch = last(useMatchRoutes()); + const router = useRouter(); + if (!lastRouteMatch) { + throw new Error('No route was matched'); + } + + return router.getRoutePath(lastRouteMatch.route); +} diff --git a/packages/kbn-typed-react-router-config/src/use_router.tsx b/packages/kbn-typed-react-router-config/src/use_router.tsx new file mode 100644 index 00000000000000..b54530ed0fbdb4 --- /dev/null +++ b/packages/kbn-typed-react-router-config/src/use_router.tsx @@ -0,0 +1,30 @@ +/* + * 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 React, { createContext, useContext } from 'react'; +import { Route, Router } from './types'; + +const RouterContext = createContext | undefined>(undefined); + +export const RouterContextProvider = ({ + router, + children, +}: { + router: Router; + children: React.ReactElement; +}) => {children}; + +export function useRouter(): Router { + const router = useContext(RouterContext); + + if (!router) { + throw new Error('Router not found in context'); + } + + return router; +} diff --git a/packages/kbn-typed-react-router-config/tsconfig.browser.json b/packages/kbn-typed-react-router-config/tsconfig.browser.json new file mode 100644 index 00000000000000..1de1603fec2866 --- /dev/null +++ b/packages/kbn-typed-react-router-config/tsconfig.browser.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "incremental": true, + "outDir": "./target_web", + "stripInternal": true, + "declaration": false, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src", + "types": [ + "node", + "jest" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-typed-react-router-config/tsconfig.json b/packages/kbn-typed-react-router-config/tsconfig.json new file mode 100644 index 00000000000000..fb7262aa686626 --- /dev/null +++ b/packages/kbn-typed-react-router-config/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": true, + "declarationDir": "./target_types", + "outDir": "./target_node", + "stripInternal": true, + "declaration": true, + "declarationMap": true, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-typed-react-router-config/src", + "types": [ + "node", + "jest" + ] + }, + "include": [ + "src/**/*" + ] +} 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/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index 801fa452e83323..16504cf97366ec 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -29,7 +29,6 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiCodeEditor.stopEditing": "When you're done, press Escape to stop editing.", "euiCodeEditor.stopInteracting": "When you're done, press Escape to stop interacting with the code.", "euiCollapsedItemActions.allActions": "All actions", - "euiCollapsibleNav.closeButtonLabel": "close", "euiColorPicker.alphaLabel": "Alpha channel (opacity) value", "euiColorPicker.closeLabel": "Press the down key to open a popover containing color options", "euiColorPicker.colorErrorMessage": "Invalid color value", @@ -45,6 +44,7 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiColorStopThumb.stopErrorMessage": "Value is out of range", "euiColorStopThumb.stopLabel": "Stop value", "euiColorStops.screenReaderAnnouncement": [Function], + "euiColumnActions.hideColumn": "Hide column", "euiColumnActions.moveLeft": "Move left", "euiColumnActions.moveRight": "Move right", "euiColumnActions.sort": [Function], @@ -75,6 +75,9 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiComboBoxOptionsList.noMatchingOptions": [Function], "euiComboBoxPill.removeSelection": [Function], "euiCommonlyUsedTimeRanges.legend": "Commonly used", + "euiControlBar.customScreenReaderAnnouncement": [Function], + "euiControlBar.screenReaderAnnouncement": "There is a new region landmark with page level controls at the end of the document.", + "euiControlBar.screenReaderHeading": "Page level controls", "euiDataGrid.ariaLabel": [Function], "euiDataGrid.ariaLabelGridPagination": [Function], "euiDataGrid.ariaLabelledBy": [Function], @@ -100,11 +103,11 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiFieldPassword.showPassword": "Show password as plain text. Note: this will visually expose your password on the screen.", "euiFilePicker.clearSelectedFiles": "Clear selected files", "euiFilePicker.filesSelected": "files selected", + "euiFilePicker.removeSelected": "Remove", "euiFilterButton.filterBadge": [Function], "euiFlyout.closeAriaLabel": "Close this dialog", "euiForm.addressFormErrors": "Please address the highlighted errors.", "euiFormControlLayoutClearButton.label": "Clear input", - "euiHeaderAlert.dismiss": "Dismiss", "euiHeaderLinks.appNavigation": "App menu", "euiHeaderLinks.openNavigationMenu": "Open menu", "euiHue.label": "Select the HSV color mode \\"hue\\" value", @@ -134,11 +137,16 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiNotificationEventReadButton.markAsReadAria": [Function], "euiNotificationEventReadButton.markAsUnread": "Mark as unread", "euiNotificationEventReadButton.markAsUnreadAria": [Function], + "euiNotificationEventReadIcon.read": "Read", + "euiNotificationEventReadIcon.readAria": [Function], + "euiNotificationEventReadIcon.unread": "Unread", + "euiNotificationEventReadIcon.unreadAria": [Function], "euiPagination.disabledNextPage": "Next page", "euiPagination.disabledPreviousPage": "Previous page", "euiPagination.firstRangeAriaLabel": [Function], "euiPagination.lastRangeAriaLabel": [Function], "euiPagination.nextPage": [Function], + "euiPagination.pageOfTotalCompressed": [Function], "euiPagination.previousPage": [Function], "euiPaginationButton.longPageString": [Function], "euiPaginationButton.shortPageString": [Function], @@ -211,19 +219,16 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiSuperUpdateButton.refreshButtonLabel": "Refresh", "euiSuperUpdateButton.updateButtonLabel": "Update", "euiSuperUpdateButton.updatingButtonLabel": "Updating", - "euiTableHeaderCell.clickForAscending": "Click to sort in ascending order", - "euiTableHeaderCell.clickForDescending": "Click to sort in descending order", - "euiTableHeaderCell.clickForUnsort": "Click to unsort", - "euiTableHeaderCell.titleTextWithSort": [Function], + "euiTableHeaderCell.titleTextWithDesc": [Function], "euiTablePagination.rowsPerPage": "Rows per page", "euiTablePagination.rowsPerPageOption": [Function], "euiTableSortMobile.sorting": "Sorting", "euiToast.dismissToast": "Dismiss toast", "euiToast.newNotification": "A new notification appears", "euiToast.notification": "Notification", - "euiTour.closeTour": "Close tour", - "euiTour.endTour": "End tour", - "euiTour.skipTour": "Skip tour", + "euiTourStep.closeTour": "Close tour", + "euiTourStep.endTour": "End tour", + "euiTourStep.skipTour": "Skip tour", "euiTourStepIndicator.ariaLabel": [Function], "euiTourStepIndicator.isActive": "active", "euiTourStepIndicator.isComplete": "complete", diff --git a/src/core/public/i18n/i18n_eui_mapping.tsx b/src/core/public/i18n/i18n_eui_mapping.tsx index 1cccc4d94a78dd..b7fbf8f91cc4e5 100644 --- a/src/core/public/i18n/i18n_eui_mapping.tsx +++ b/src/core/public/i18n/i18n_eui_mapping.tsx @@ -9,13 +9,14 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTokensObject } from '@elastic/eui'; interface EuiValues { [key: string]: any; } export const getEuiContextMapping = () => { - const euiContextMapping = { + const euiContextMapping: EuiTokensObject = { 'euiAccordion.isLoading': i18n.translate('core.euiAccordion.isLoading', { defaultMessage: 'Loading', }), @@ -143,12 +144,6 @@ export const getEuiContextMapping = () => { 'ARIA label and tooltip content describing a button that expands an actions menu', } ), - 'euiCollapsibleNav.closeButtonLabel': i18n.translate( - 'core.euiCollapsibleNav.closeButtonLabel', - { - defaultMessage: 'close', - } - ), 'euiColorPicker.screenReaderAnnouncement': i18n.translate( 'core.euiColorPicker.screenReaderAnnouncement', { @@ -235,6 +230,9 @@ export const getEuiContextMapping = () => { 'euiColumnActions.moveRight': i18n.translate('core.euiColumnActions.moveRight', { defaultMessage: 'Move right', }), + 'euiColumnActions.hideColumn': i18n.translate('core.euiColumnActions.hideColumn', { + defaultMessage: 'Hide column', + }), 'euiColumnSelector.hideAll': i18n.translate('core.euiColumnSelector.hideAll', { defaultMessage: 'Hide all', }), @@ -368,6 +366,22 @@ export const getEuiContextMapping = () => { 'euiCommonlyUsedTimeRanges.legend': i18n.translate('core.euiCommonlyUsedTimeRanges.legend', { defaultMessage: 'Commonly used', }), + 'euiControlBar.screenReaderHeading': i18n.translate('core.euiControlBar.screenReaderHeading', { + defaultMessage: 'Page level controls', + }), + 'euiControlBar.customScreenReaderAnnouncement': ({ landmarkHeading }: EuiValues) => + i18n.translate('core.euiControlBar.customScreenReaderAnnouncement', { + defaultMessage: + 'There is a new region landmark called {landmarkHeading} with page level controls at the end of the document.', + values: { landmarkHeading }, + }), + 'euiControlBar.screenReaderAnnouncement': i18n.translate( + 'core.euiControlBar.screenReaderAnnouncement', + { + defaultMessage: + 'There is a new region landmark with page level controls at the end of the document.', + } + ), 'euiDataGrid.screenReaderNotice': i18n.translate('core.euiDataGrid.screenReaderNotice', { defaultMessage: 'Cell contains interactive content.', }), @@ -500,6 +514,9 @@ export const getEuiContextMapping = () => { 'euiFilePicker.filesSelected': i18n.translate('core.euiFilePicker.filesSelected', { defaultMessage: 'files selected', }), + 'euiFilePicker.removeSelected': i18n.translate('core.euiFilePicker.removeSelected', { + defaultMessage: 'Remove', + }), 'euiFilterButton.filterBadge': ({ count, hasActiveFilters }: EuiValues) => i18n.translate('core.euiFilterButton.filterBadge', { defaultMessage: '${count} ${filterCountLabel} filters', @@ -518,10 +535,6 @@ export const getEuiContextMapping = () => { description: 'ARIA label on a button that removes any entry in a form field', } ), - 'euiHeaderAlert.dismiss': i18n.translate('core.euiHeaderAlert.dismiss', { - defaultMessage: 'Dismiss', - description: 'ARIA label on a button that dismisses/removes a notification', - }), 'euiHeaderLinks.appNavigation': i18n.translate('core.euiHeaderLinks.appNavigation', { defaultMessage: 'App menu', description: 'ARIA label on a `nav` element', @@ -669,6 +682,25 @@ export const getEuiContextMapping = () => { defaultMessage: 'Mark as unread', } ), + 'euiNotificationEventReadIcon.readAria': ({ eventName }: EuiValues) => + i18n.translate('core.euiNotificationEventReadIcon.readAria', { + defaultMessage: '{eventName} is read', + values: { eventName }, + }), + 'euiNotificationEventReadIcon.unreadAria': ({ eventName }: EuiValues) => + i18n.translate('core.euiNotificationEventReadIcon.unreadAria', { + defaultMessage: '{eventName} is unread', + values: { eventName }, + }), + 'euiNotificationEventReadIcon.read': i18n.translate('core.euiNotificationEventReadIcon.read', { + defaultMessage: 'Read', + }), + 'euiNotificationEventReadIcon.unread': i18n.translate( + 'core.euiNotificationEventReadIcon.unread', + { + defaultMessage: 'Unread', + } + ), 'euiNotificationEventMessages.accordionHideText': i18n.translate( 'core.euiNotificationEventMessages.accordionHideText', { @@ -680,6 +712,13 @@ export const getEuiContextMapping = () => { defaultMessage: 'Next page, {page}', values: { page }, }), + 'euiPagination.pageOfTotalCompressed': ({ page, total }: EuiValues) => ( + + ), 'euiPagination.previousPage': ({ page }: EuiValues) => i18n.translate('core.euiPagination.previousPage', { defaultMessage: 'Previous page, {page}', @@ -1043,29 +1082,10 @@ export const getEuiContextMapping = () => { description: 'Displayed in a button that updates based on date picked', } ), - 'euiTableHeaderCell.clickForAscending': i18n.translate( - 'core.euiTableHeaderCell.clickForAscending', - { - defaultMessage: 'Click to sort in ascending order', - description: 'Displayed in a button that toggles a table sorting', - } - ), - 'euiTableHeaderCell.clickForDescending': i18n.translate( - 'core.euiTableHeaderCell.clickForDescending', - { - defaultMessage: 'Click to sort in descending order', - description: 'Displayed in a button that toggles a table sorting', - } - ), - 'euiTableHeaderCell.clickForUnsort': i18n.translate('core.euiTableHeaderCell.clickForUnsort', { - defaultMessage: 'Click to unsort', - description: 'Displayed in a button that toggles a table sorting', - }), - 'euiTableHeaderCell.titleTextWithSort': ({ innerText, ariaSortValue }: EuiValues) => - i18n.translate('core.euiTableHeaderCell.titleTextWithSort', { - defaultMessage: '{innerText}; Sorted in {ariaSortValue} order', - values: { innerText, ariaSortValue }, - description: 'Text describing the table sort order', + 'euiTableHeaderCell.titleTextWithDesc': ({ innerText, description }: EuiValues) => + i18n.translate('core.euiTableHeaderCell.titleTextWithDesc', { + defaultMessage: '{innerText}; {description}', + values: { innerText, description }, }), 'euiTablePagination.rowsPerPage': i18n.translate('core.euiTablePagination.rowsPerPage', { defaultMessage: 'Rows per page', @@ -1091,15 +1111,6 @@ export const getEuiContextMapping = () => { defaultMessage: 'Notification', description: 'ARIA label on an element containing a notification', }), - 'euiTour.endTour': i18n.translate('core.euiTour.endTour', { - defaultMessage: 'End tour', - }), - 'euiTour.skipTour': i18n.translate('core.euiTour.skipTour', { - defaultMessage: 'Skip tour', - }), - 'euiTour.closeTour': i18n.translate('core.euiTour.closeTour', { - defaultMessage: 'Close tour', - }), 'euiTourStepIndicator.isActive': i18n.translate('core.euiTourStepIndicator.isActive', { defaultMessage: 'active', description: 'Text for an active tour step', @@ -1112,6 +1123,15 @@ export const getEuiContextMapping = () => { defaultMessage: 'incomplete', description: 'Text for an incomplete tour step', }), + 'euiTourStep.endTour': i18n.translate('core.euiTourStep.endTour', { + defaultMessage: 'End tour', + }), + 'euiTourStep.skipTour': i18n.translate('core.euiTourStep.skipTour', { + defaultMessage: 'Skip tour', + }), + 'euiTourStep.closeTour': i18n.translate('core.euiTourStep.closeTour', { + defaultMessage: 'Close tour', + }), 'euiTourStepIndicator.ariaLabel': ({ status, number }: EuiValues) => i18n.translate('core.euiTourStepIndicator.ariaLabel', { defaultMessage: 'Step {number} {status}', 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/bundle_routes/file_hash.test.ts b/src/core/server/core_app/bundle_routes/file_hash.test.ts index ef24ebe0630572..0de63d7409ed94 100644 --- a/src/core/server/core_app/bundle_routes/file_hash.test.ts +++ b/src/core/server/core_app/bundle_routes/file_hash.test.ts @@ -19,8 +19,7 @@ const mockedCache = (): jest.Mocked => ({ set: jest.fn(), }); -// FLAKY: https://github.com/elastic/kibana/issues/105174 -describe.skip('getFileHash', () => { +describe('getFileHash', () => { const sampleFilePath = resolve(__dirname, 'foo.js'); const fd = 42; const stats: Stats = { ino: 42, size: 9000 } as any; @@ -68,6 +67,8 @@ describe.skip('getFileHash', () => { await getFileHash(cache, sampleFilePath, stats, fd); expect(cache.set).toHaveBeenCalledTimes(1); - expect(cache.set).toHaveBeenCalledWith(`${sampleFilePath}-${stats.ino}`, computedHashPromise); + expect(cache.set).toHaveBeenCalledWith(`${sampleFilePath}-${stats.ino}`, expect.any(Promise)); + const promiseValue = await cache.set.mock.calls[0][1]; + expect(promiseValue).toEqual('computed-hash'); }); }); 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/core/server/preboot/index.ts b/src/core/server/preboot/index.ts new file mode 100644 index 00000000000000..2b7f25538dcb1c --- /dev/null +++ b/src/core/server/preboot/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +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(