mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Conflicts: # .github/CODEOWNERS # src/core/server/ui_settings/ui_settings_service.ts
This commit is contained in:
parent
0fd063904a
commit
9aa5c806e1
194 changed files with 5859 additions and 1201 deletions
0
.github/CODEOWNERS
vendored
0
.github/CODEOWNERS
vendored
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
elasticsearch: ElasticsearchServicePreboot;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
http: HttpServicePreboot;
|
||||
```
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface CorePreboot
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [elasticsearch](./kibana-plugin-core-server.corepreboot.elasticsearch.md) | <code>ElasticsearchServicePreboot</code> | [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) |
|
||||
| [http](./kibana-plugin-core-server.corepreboot.http.md) | <code>HttpServicePreboot</code> | [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) |
|
||||
| [preboot](./kibana-plugin-core-server.corepreboot.preboot.md) | <code>PrebootServicePreboot</code> | [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
preboot: PrebootServicePreboot;
|
||||
```
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## CoreSetup interface
|
||||
|
||||
Context passed to the plugins `setup` method.
|
||||
Context passed to the `setup` method of `standard` plugins.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -21,4 +21,5 @@ export interface DiscoveredPlugin
|
|||
| [optionalPlugins](./kibana-plugin-core-server.discoveredplugin.optionalplugins.md) | <code>readonly PluginName[]</code> | 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) | <code>readonly PluginName[]</code> | List of plugin ids that this plugin's UI code imports modules from that are not in <code>requiredPlugins</code>. |
|
||||
| [requiredPlugins](./kibana-plugin-core-server.discoveredplugin.requiredplugins.md) | <code>readonly PluginName[]</code> | 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) | <code>PluginType</code> | Type of the plugin, defaults to <code>standard</code>. |
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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`<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly type: PluginType;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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`<!-- -->).
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly credentialsSpecified: boolean;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly hosts: string[];
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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`<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface ElasticsearchConfigPreboot
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [credentialsSpecified](./kibana-plugin-core-server.elasticsearchconfigpreboot.credentialsspecified.md) | <code>boolean</code> | Indicates whether Elasticsearch configuration includes credentials (<code>username</code>, <code>password</code> or <code>serviceAccountToken</code>). |
|
||||
| [hosts](./kibana-plugin-core-server.elasticsearchconfigpreboot.hosts.md) | <code>string[]</code> | 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. |
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly config: Readonly<ElasticsearchConfigPreboot>;
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
```js
|
||||
const { hosts, credentialsSpecified } = core.elasticsearch.config;
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient;
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
```js
|
||||
const client = elasticsearch.createClient('my-app-name', config);
|
||||
const data = await client.asInternalUser.search();
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md)
|
||||
|
||||
## ElasticsearchServicePreboot interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface ElasticsearchServicePreboot
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [config](./kibana-plugin-core-server.elasticsearchservicepreboot.config.md) | <code>Readonly<ElasticsearchConfigPreboot></code> | A limited set of Elasticsearch configuration entries. |
|
||||
| [createClient](./kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md) | <code>(type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient</code> | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md)<!-- -->. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
basePath: IBasePath;
|
||||
```
|
|
@ -0,0 +1,82 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```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) | <code>IBasePath</code> | 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 <code>preboot</code> [IRouter](./kibana-plugin-core-server.irouter.md) instance for a particular top-level path and register handler functions for any number of nested routes. |
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
registerRoutes(path: string, callback: (router: IRouter) => void): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| path | <code>string</code> | |
|
||||
| callback | <code>(router: IRouter) => void</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`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' }));
|
||||
});
|
||||
|
||||
```
|
||||
|
|
@ -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 <code>registerProvider</code> method.<!-- -->Plugins having the responsibility to restrict capabilities depending on a given context should register their capabilities switcher using the <code>registerSwitcher</code> 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 <code>setup</code> method. |
|
||||
| [CorePreboot](./kibana-plugin-core-server.corepreboot.md) | Context passed to the <code>setup</code> method of <code>preboot</code> plugins. |
|
||||
| [CoreSetup](./kibana-plugin-core-server.coresetup.md) | Context passed to the <code>setup</code> method of <code>standard</code> plugins. |
|
||||
| [CoreStart](./kibana-plugin-core-server.corestart.md) | Context passed to the plugins <code>start</code> 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 <code>preboot</code> plugins at <code>setup</code>. |
|
||||
| [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 <code>preboot</code> stage. This functionality allows Kibana to serve user requests even before Kibana becomes fully operational. Only Core and <code>preboot</code> 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 <code>hapi</code> 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 <code>PluginInitializer</code>. |
|
||||
| [Plugin](./kibana-plugin-core-server.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code> for a <code>standard</code> 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 <code>PluginInitializer</code> for a <code>preboot</code> 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 <code>resolveCapabilities</code> method of [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `server` directory should conform
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type PluginInitializer<TSetup, TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart> | AsyncPlugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
||||
export declare type PluginInitializer<TSetup, TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart> | PrebootPlugin<TSetup, TPluginsSetup> | AsyncPlugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
||||
```
|
||||
|
|
|
@ -11,5 +11,6 @@ env: {
|
|||
mode: EnvironmentMode;
|
||||
packageInfo: Readonly<PackageInfo>;
|
||||
instanceUuid: string;
|
||||
configs: readonly string[];
|
||||
};
|
||||
```
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface PluginInitializerContext<ConfigSchema = unknown>
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | <code>{</code><br/><code> legacy: {</code><br/><code> globalConfig$: Observable<SharedGlobalConfig>;</code><br/><code> get: () => SharedGlobalConfig;</code><br/><code> };</code><br/><code> create: <T = ConfigSchema>() => Observable<T>;</code><br/><code> get: <T = ConfigSchema>() => T;</code><br/><code> }</code> | Accessors for the plugin's configuration |
|
||||
| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | <code>{</code><br/><code> mode: EnvironmentMode;</code><br/><code> packageInfo: Readonly<PackageInfo>;</code><br/><code> instanceUuid: string;</code><br/><code> }</code> | |
|
||||
| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | <code>{</code><br/><code> mode: EnvironmentMode;</code><br/><code> packageInfo: Readonly<PackageInfo>;</code><br/><code> instanceUuid: string;</code><br/><code> configs: readonly string[];</code><br/><code> }</code> | |
|
||||
| [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | <code>LoggerFactory</code> | instance already bound to the plugin's logging context |
|
||||
| [opaqueId](./kibana-plugin-core-server.plugininitializercontext.opaqueid.md) | <code>PluginOpaqueId</code> | |
|
||||
|
||||
|
|
|
@ -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) | <code>readonly PluginName[]</code> | 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) | <code>boolean</code> | Specifies whether plugin includes some server-side specific functionality. |
|
||||
| [serviceFolders](./kibana-plugin-core-server.pluginmanifest.servicefolders.md) | <code>readonly string[]</code> | 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) | <code>PluginType</code> | Type of the plugin, defaults to <code>standard</code>. |
|
||||
| [ui](./kibana-plugin-core-server.pluginmanifest.ui.md) | <code>boolean</code> | Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via <code>public/ui_plugin.js</code> file. |
|
||||
| [version](./kibana-plugin-core-server.pluginmanifest.version.md) | <code>string</code> | Version of the plugin. |
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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`<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly type: PluginType;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginType](./kibana-plugin-core-server.plugintype.md)
|
||||
|
||||
## PluginType enum
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare enum PluginType
|
||||
```
|
||||
|
||||
## Enumeration Members
|
||||
|
||||
| Member | Value | Description |
|
||||
| --- | --- | --- |
|
||||
| preboot | <code>"preboot"</code> | Preboot plugins are special-purpose plugins that only function during preboot stage. |
|
||||
| standard | <code>"standard"</code> | Standard plugins are plugins that start to function as soon as Kibana is fully booted and are active until it shuts down. |
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface PrebootPlugin<TSetup = void, TPluginsSetup extends object = object>
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [setup(core, plugins)](./kibana-plugin-core-server.prebootplugin.setup.md) | |
|
||||
| [stop()](./kibana-plugin-core-server.prebootplugin.stop.md) | |
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
setup(core: CorePreboot, plugins: TPluginsSetup): TSetup;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| core | <code>CorePreboot</code> | |
|
||||
| plugins | <code>TPluginsSetup</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`TSetup`
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
stop?(): void;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly holdSetupUntilResolved: (reason: string, promise: Promise<{
|
||||
shouldReloadConfig: boolean;
|
||||
} | undefined>) => void;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly isSetupOnHold: () => boolean;
|
||||
```
|
|
@ -0,0 +1,45 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```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) | <code>(reason: string, promise: Promise<{</code><br/><code> shouldReloadConfig: boolean;</code><br/><code> } | undefined>) => void</code> | Registers a <code>Promise</code> as a precondition before Kibana can proceed to <code>setup</code>. This method can be invoked multiple times and from multiple <code>preboot</code> plugins. Kibana will proceed to <code>setup</code> only when all registered <code>Promises</code> instances are resolved, or it will shut down if any of them is rejected. |
|
||||
| [isSetupOnHold](./kibana-plugin-core-server.prebootservicepreboot.issetuponhold.md) | <code>() => boolean</code> | Indicates whether Kibana is currently on hold and cannot proceed to <code>setup</code> yet. |
|
||||
|
3
examples/preboot_example/README.md
Normal file
3
examples/preboot_example/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `prebootExample` plugin
|
||||
|
||||
The example of the `preboot` plugin.
|
16
examples/preboot_example/kibana.json
Normal file
16
examples/preboot_example/kibana.json
Normal file
|
@ -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": []
|
||||
}
|
218
examples/preboot_example/public/app.tsx
Normal file
218
examples/preboot_example/public/app.tsx
Normal file
|
@ -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<string | null>(null);
|
||||
|
||||
const [isSetupModeActive, setIsSetupModeActive] = useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
http.get<{ isSetupModeActive: boolean }>('/api/preboot/state').then(
|
||||
(response) => setIsSetupModeActive(response.isSetupModeActive),
|
||||
(err: IHttpFetchError) => setIsSetupModeActive(false)
|
||||
);
|
||||
}, [http]);
|
||||
|
||||
if (!isSetupModeActive) {
|
||||
return (
|
||||
<EuiPageTemplate
|
||||
restrictWidth={false}
|
||||
template="empty"
|
||||
pageHeader={{
|
||||
iconType: 'logoElastic',
|
||||
pageTitle: 'Welcome to Elastic',
|
||||
}}
|
||||
>
|
||||
<EuiPanel>
|
||||
<EuiText>Kibana server is not ready yet.</EuiText>
|
||||
</EuiPanel>
|
||||
</EuiPageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPageTemplate
|
||||
restrictWidth={false}
|
||||
template="empty"
|
||||
pageHeader={{
|
||||
iconType: 'logoElastic',
|
||||
pageTitle: 'Welcome to Elastic',
|
||||
}}
|
||||
>
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction={'column'}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
placeholder="Config key"
|
||||
value={configKeyValue.key}
|
||||
onChange={(e) => {
|
||||
setConfigKeyValue({ ...configKeyValue, key: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
placeholder="Config value"
|
||||
value={configKeyValue.value}
|
||||
onChange={(e) => {
|
||||
setConfigKeyValue({ ...configKeyValue, value: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
size="m"
|
||||
color={'danger'}
|
||||
onClick={onWriteToken}
|
||||
disabled={!configKeyValue.key || !configKeyValue.value}
|
||||
>
|
||||
Write config
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction={'column'}>
|
||||
<EuiFlexItem>
|
||||
<EuiText>Token from config: {token}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
size="m"
|
||||
color={'danger'}
|
||||
onClick={() => onCompleteSetup({ shouldReloadConfig: true })}
|
||||
>
|
||||
Reload config and proceed to `setup`
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
size="m"
|
||||
color={'primary'}
|
||||
onClick={() => onCompleteSetup({ shouldReloadConfig: false })}
|
||||
>
|
||||
DO NOT reload config and proceed to `setup`
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction={'column'}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
placeholder="elasticsearch.hosts"
|
||||
value={elasticsearchConfig.host}
|
||||
onChange={(e) => {
|
||||
setElasticsearchConfig({ ...elasticsearchConfig, host: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
placeholder="elasticsearch.username"
|
||||
value={elasticsearchConfig.username}
|
||||
onChange={(e) => {
|
||||
setElasticsearchConfig({
|
||||
...elasticsearchConfig,
|
||||
username: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
placeholder="elasticsearch.password"
|
||||
value={elasticsearchConfig.password}
|
||||
onChange={(e) => {
|
||||
setElasticsearchConfig({
|
||||
...elasticsearchConfig,
|
||||
password: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
size="m"
|
||||
color={'danger'}
|
||||
onClick={onConnect}
|
||||
disabled={
|
||||
!elasticsearchConfig.host ||
|
||||
!elasticsearchConfig.username ||
|
||||
!elasticsearchConfig.password
|
||||
}
|
||||
>
|
||||
Connect
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiCodeBlock language="json" paddingSize="s" isCopyable>
|
||||
{connectResponse ?? ''}
|
||||
</EuiCodeBlock>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiPageTemplate>
|
||||
);
|
||||
};
|
11
examples/preboot_example/public/config.ts
Normal file
11
examples/preboot_example/public/config.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 interface ConfigType {
|
||||
token?: string;
|
||||
}
|
13
examples/preboot_example/public/index.ts
Normal file
13
examples/preboot_example/public/index.ts
Normal file
|
@ -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);
|
35
examples/preboot_example/public/plugin.tsx
Normal file
35
examples/preboot_example/public/plugin.tsx
Normal file
|
@ -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<void, void, {}, {}> {
|
||||
#config: ConfigType;
|
||||
constructor(initializerContext: PluginInitializerContext<ConfigType>) {
|
||||
this.#config = initializerContext.config.get<ConfigType>();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
core.application.register({
|
||||
id: 'prebootExample',
|
||||
title: 'Preboot Example',
|
||||
appRoute: '/',
|
||||
chromeless: true,
|
||||
mount: (params) => {
|
||||
ReactDOM.render(<App http={core.http} token={this.#config.token} />, params.element);
|
||||
return () => ReactDOM.unmountComponentAtNode(params.element);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {}
|
||||
}
|
18
examples/preboot_example/server/config.ts
Normal file
18
examples/preboot_example/server/config.ts
Normal file
|
@ -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<typeof ConfigSchema>;
|
||||
|
||||
export const ConfigSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
token: schema.maybe(schema.string()),
|
||||
skipSetup: schema.boolean({ defaultValue: false }),
|
||||
});
|
20
examples/preboot_example/server/index.ts
Normal file
20
examples/preboot_example/server/index.ts
Normal file
|
@ -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<TypeOf<typeof ConfigSchema>> = {
|
||||
schema: ConfigSchema,
|
||||
exposeToBrowser: { token: true },
|
||||
};
|
||||
|
||||
export const plugin = (context: PluginInitializerContext) => new PrebootExamplePlugin(context);
|
135
examples/preboot_example/server/plugin.ts
Normal file
135
examples/preboot_example/server/plugin.ts
Normal file
|
@ -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<ConfigType>;
|
||||
constructor(initializerContext: PluginInitializerContext<ConfigType>) {
|
||||
this.#initializerContext = initializerContext;
|
||||
}
|
||||
|
||||
public setup(core: CorePreboot) {
|
||||
const { skipSetup } = this.#initializerContext.config.get<ConfigType>();
|
||||
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() {}
|
||||
}
|
12
examples/preboot_example/tsconfig.json
Normal file
12
examples/preboot_example/tsconfig.json
Normal file
|
@ -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" }]
|
||||
}
|
|
@ -37,6 +37,7 @@ const getRawConfigProvider = (rawConfig: Record<string, any>) =>
|
|||
|
||||
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' });
|
||||
|
|
|
@ -29,6 +29,14 @@ import { LegacyObjectToConfigAdapter } from './legacy';
|
|||
/** @internal */
|
||||
export type IConfigService = PublicMethodsOf<ConfigService>;
|
||||
|
||||
/** @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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -330,6 +330,7 @@ describe('myPlugin', () => {
|
|||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeAll(async () => {
|
||||
root = kbnTestServer.createRoot();
|
||||
await root.preboot();
|
||||
await root.setup();
|
||||
await root.start();
|
||||
}, 30000);
|
||||
|
@ -375,6 +376,7 @@ describe('myPlugin', () => {
|
|||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeAll(async () => {
|
||||
root = kbnTestServer.createRoot();
|
||||
await root.preboot();
|
||||
await root.setup();
|
||||
await root.start();
|
||||
}, 30000);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -36,6 +36,7 @@ const createCapabilitiesMock = (): Capabilities => {
|
|||
type CapabilitiesServiceContract = PublicMethodsOf<CapabilitiesService>;
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<CapabilitiesServiceContract> = {
|
||||
preboot: jest.fn(),
|
||||
setup: jest.fn().mockReturnValue(createSetupContractMock()),
|
||||
start: jest.fn().mockReturnValue(createStartContractMock()),
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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<Capabilities>;
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<InternalContextPreboot> = {
|
||||
createContextContainer: jest.fn().mockImplementation(() => contextMock.create(mockContext)),
|
||||
};
|
||||
return prebootContract;
|
||||
};
|
||||
|
||||
const createSetupContractMock = (mockContext = {}) => {
|
||||
const setupContract: jest.Mocked<ContextSetup> = {
|
||||
createContextContainer: jest.fn().mockImplementation(() => contextMock.create(mockContext)),
|
||||
|
@ -21,13 +28,16 @@ const createSetupContractMock = (mockContext = {}) => {
|
|||
type ContextServiceContract = PublicMethodsOf<ContextService>;
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<ContextServiceContract> = {
|
||||
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,
|
||||
};
|
||||
|
|
|
@ -14,10 +14,23 @@ import { CoreContext } from '../core_context';
|
|||
const pluginDependencies = new Map<PluginOpaqueId, PluginOpaqueId[]>();
|
||||
|
||||
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);
|
||||
|
|
|
@ -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<PluginOpaqueId, PluginOpaqueId[]>;
|
||||
}
|
||||
|
@ -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<PluginOpaqueId, PluginOpaqueId[]>
|
||||
) {
|
||||
return {
|
||||
createContextContainer: () => {
|
||||
return new ContextContainer(pluginDependencies, this.core.coreId);
|
||||
|
@ -27,6 +39,9 @@ export class ContextService {
|
|||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type InternalContextPreboot = ContextSetup;
|
||||
|
||||
/**
|
||||
* {@inheritdoc IContextContainer}
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<typeof mockCoreContext.create>;
|
||||
let coreApp: CoreApp;
|
||||
let internalCorePreboot: ReturnType<typeof coreMock.createInternalPreboot>;
|
||||
let prebootHTTPResourcesRegistrar: ReturnType<typeof httpResourcesMock.createRegistrar>;
|
||||
let internalCoreSetup: ReturnType<typeof coreMock.createInternalSetup>;
|
||||
let httpResourcesRegistrar: ReturnType<typeof httpResourcesMock.createRegistrar>;
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
@ -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<IKibanaResponse>;
|
||||
}
|
||||
|
||||
/** @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,50 +86,12 @@ export class CoreApp {
|
|||
});
|
||||
});
|
||||
|
||||
// remove trailing slash catch-all
|
||||
router.get(
|
||||
{
|
||||
path: '/{path*}',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
path: schema.maybe(schema.string()),
|
||||
}),
|
||||
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
|
||||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
const { query, params } = req;
|
||||
const { path } = params;
|
||||
if (!path || !path.endsWith('/') || path.startsWith('/')) {
|
||||
return res.notFound();
|
||||
}
|
||||
|
||||
const basePath = httpSetup.basePath.get(req);
|
||||
let rewrittenPath = path.slice(0, -1);
|
||||
if (`/${path}`.startsWith(basePath)) {
|
||||
rewrittenPath = rewrittenPath.substring(basePath.length);
|
||||
}
|
||||
|
||||
const querystring = query ? stringify(query) : undefined;
|
||||
const url = `${basePath}/${rewrittenPath}${querystring ? `?${querystring}` : ''}`;
|
||||
|
||||
return res.redirected({
|
||||
headers: {
|
||||
location: url,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
router.get({ path: '/core', validate: false }, async (context, req, res) =>
|
||||
res.ok({ body: { version: '0.0.1' } })
|
||||
);
|
||||
|
||||
registerBundleRoutes({
|
||||
this.registerCommonDefaultRoutes({
|
||||
basePath: coreSetup.http.basePath,
|
||||
httpResources: resources,
|
||||
router,
|
||||
uiPlugins,
|
||||
packageInfo: this.env.packageInfo,
|
||||
serverBasePath: coreSetup.http.basePath.serverBasePath,
|
||||
onResourceNotFound: async (res) => res.notFound(),
|
||||
});
|
||||
|
||||
resources.register(
|
||||
|
@ -129,10 +126,65 @@ export class CoreApp {
|
|||
);
|
||||
}
|
||||
|
||||
private registerStaticDirs(coreSetup: InternalCoreSetup) {
|
||||
coreSetup.http.registerStaticDir('/ui/{path*}', Path.resolve(__dirname, './assets'));
|
||||
private registerCommonDefaultRoutes({
|
||||
router,
|
||||
basePath,
|
||||
uiPlugins,
|
||||
onResourceNotFound,
|
||||
httpResources,
|
||||
}: CommonRoutesParams) {
|
||||
// catch-all route
|
||||
httpResources.register(
|
||||
{
|
||||
path: '/{path*}',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
path: schema.maybe(schema.string()),
|
||||
}),
|
||||
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
|
||||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
const { query, params } = req;
|
||||
const { path } = params;
|
||||
if (!path || !path.endsWith('/') || path.startsWith('/')) {
|
||||
return onResourceNotFound(res);
|
||||
}
|
||||
|
||||
coreSetup.http.registerStaticDir(
|
||||
// remove trailing slash
|
||||
const requestBasePath = basePath.get(req);
|
||||
let rewrittenPath = path.slice(0, -1);
|
||||
if (`/${path}`.startsWith(requestBasePath)) {
|
||||
rewrittenPath = rewrittenPath.substring(requestBasePath.length);
|
||||
}
|
||||
|
||||
const querystring = query ? stringify(query) : undefined;
|
||||
const url = `${requestBasePath}/${rewrittenPath}${querystring ? `?${querystring}` : ''}`;
|
||||
|
||||
return res.redirected({
|
||||
headers: {
|
||||
location: url,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
router.get({ path: '/core', validate: false }, async (context, req, res) =>
|
||||
res.ok({ body: { version: '0.0.1' } })
|
||||
);
|
||||
|
||||
registerBundleRoutes({
|
||||
router,
|
||||
uiPlugins,
|
||||
packageInfo: this.env.packageInfo,
|
||||
serverBasePath: basePath.serverBasePath,
|
||||
});
|
||||
}
|
||||
|
||||
private registerStaticDirs(core: InternalCoreSetup | InternalCorePreboot) {
|
||||
core.http.registerStaticDir('/ui/{path*}', Path.resolve(__dirname, './assets'));
|
||||
|
||||
core.http.registerStaticDir(
|
||||
'/node_modules/@kbn/ui-framework/dist/{path*}',
|
||||
fromRoot('node_modules/@kbn/ui-framework/dist')
|
||||
);
|
||||
|
|
|
@ -26,12 +26,13 @@ describe('bundle routes', () => {
|
|||
let logger: ReturnType<typeof loggingSystemMock.create>;
|
||||
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 () => {
|
||||
|
|
|
@ -20,6 +20,7 @@ describe('Core app routes', () => {
|
|||
},
|
||||
});
|
||||
|
||||
await root.preboot();
|
||||
await root.setup();
|
||||
await root.start();
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ describe('default route provider', () => {
|
|||
},
|
||||
});
|
||||
|
||||
await root.preboot();
|
||||
await root.setup();
|
||||
await root.start();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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<ElasticsearchServicePreboot>;
|
||||
|
||||
export interface MockedElasticSearchServiceSetup {
|
||||
legacy: {
|
||||
config$: BehaviorSubject<ElasticsearchConfig>;
|
||||
|
@ -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<ElasticsearchService>;
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<ElasticsearchServiceContract> = {
|
||||
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,
|
||||
|
|
|
@ -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<typeof elasticsearchClientMock.createC
|
|||
let mockLegacyClusterClientInstance: ReturnType<
|
||||
typeof elasticsearchServiceMock.createLegacyCustomClusterClient
|
||||
>;
|
||||
|
||||
let mockConfig$: BehaviorSubject<any>;
|
||||
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]`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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<InternalElasticsearchServicePreboot> {
|
||||
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<InternalElasticsearchServiceSetup> {
|
||||
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<ElasticsearchClientConfig> = {}
|
||||
): 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<ElasticsearchClientConfig>
|
||||
) {
|
||||
const config = clientConfig ? merge({}, baseConfig, clientConfig) : baseConfig;
|
||||
return new ClusterClient(
|
||||
config,
|
||||
this.coreContext.logger.get('elasticsearch'),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<ElasticsearchConfigPreboot>;
|
||||
|
||||
/**
|
||||
* 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<ElasticsearchClientConfig>
|
||||
) => ICustomClusterClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -77,6 +114,9 @@ export interface ElasticsearchServiceSetup {
|
|||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type InternalElasticsearchServicePreboot = ElasticsearchServicePreboot;
|
||||
|
||||
/** @internal */
|
||||
export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceSetup {
|
||||
esNodesCompatibility$: Observable<NodesVersionCompatibility>;
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 createSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<InternalEnvironmentServiceSetup> = {
|
||||
const createPrebootContractMock = () => {
|
||||
const prebootContract: jest.Mocked<InternalEnvironmentServicePreboot> = {
|
||||
instanceUuid: 'uuid',
|
||||
};
|
||||
return setupContract;
|
||||
return prebootContract;
|
||||
};
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const prebootContract: jest.Mocked<InternalEnvironmentServiceSetup> = {
|
||||
instanceUuid: 'uuid',
|
||||
};
|
||||
return prebootContract;
|
||||
};
|
||||
|
||||
type EnvironmentServiceContract = PublicMethodsOf<EnvironmentService>;
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<EnvironmentServiceContract> = {
|
||||
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,
|
||||
};
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<PathConfigType>(pathConfigDef.path).pipe(take(1)).toPromise(),
|
||||
this.configService.atPath<HttpConfigType>(httpConfigDef.path).pipe(take(1)).toPromise(),
|
||||
|
@ -73,4 +80,10 @@ export class EnvironmentService {
|
|||
instanceUuid: this.uuid,
|
||||
};
|
||||
}
|
||||
|
||||
public setup() {
|
||||
return {
|
||||
instanceUuid: this.uuid,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -41,6 +41,7 @@ describe('trace', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
await root.preboot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -167,6 +168,7 @@ describe('trace', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
await rootExecutionContextDisabled.preboot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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<InternalHttpServiceSetup['basePath']>;
|
||||
type AuthMocked = jest.Mocked<InternalHttpServiceSetup['auth']>;
|
||||
|
||||
export type HttpServicePrebootMock = jest.Mocked<HttpServicePreboot>;
|
||||
export type InternalHttpServicePrebootMock = jest.Mocked<
|
||||
Omit<InternalHttpServicePreboot, 'basePath'>
|
||||
> & { basePath: BasePathMocked };
|
||||
export type HttpServiceSetupMock = jest.Mocked<
|
||||
Omit<HttpServiceSetup, 'basePath' | 'createRouter'>
|
||||
> & {
|
||||
|
@ -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<HttpService>;
|
|||
|
||||
const createHttpServiceMock = () => {
|
||||
const mocked: jest.Mocked<HttpServiceContract> = {
|
||||
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,
|
||||
|
|
|
@ -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<HttpConfigType> = {}) => {
|
|||
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,9 +316,70 @@ test('register route handler', async () => {
|
|||
expect(registerRouterMock).toHaveBeenLastCalledWith(router);
|
||||
});
|
||||
|
||||
test('returns http server contract on setup', async () => {
|
||||
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, options: { someOption: true } };
|
||||
const httpServer = {
|
||||
server: fakeHapiServer,
|
||||
registerStaticDir: jest.fn(),
|
||||
auth: Symbol('auth'),
|
||||
basePath: Symbol('basePath'),
|
||||
csp: Symbol('csp'),
|
||||
};
|
||||
|
||||
mockHttpServer.mockImplementation(() => ({
|
||||
isListening: () => false,
|
||||
|
@ -260,10 +389,44 @@ test('returns http server contract on setup', async () => {
|
|||
}));
|
||||
|
||||
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),
|
||||
start: noop,
|
||||
stop: noop,
|
||||
}));
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
@ -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<InternalHttpServiceSetup, InternalHttpServiceStart> {
|
||||
private readonly prebootServer: HttpServer;
|
||||
private isPrebootServerStopped = false;
|
||||
private readonly httpServer: HttpServer;
|
||||
private readonly httpsRedirectServer: HttpsRedirectServer;
|
||||
private readonly config$: Observable<HttpConfig>;
|
||||
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<ExternalUrlConfigType>(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<InternalHttpServicePreboot> {
|
||||
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: <Context extends RequestHandlerContext = RequestHandlerContext>(
|
||||
|
@ -124,6 +177,8 @@ export class HttpService
|
|||
contextName: ContextName,
|
||||
provider: RequestHandlerContextProvider<Context, ContextName>
|
||||
) => 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<InternalNotReadyHttpServiceSetup | undefined> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,8 @@ export type {
|
|||
RequestHandlerContextContainer,
|
||||
RequestHandlerContextProvider,
|
||||
HttpAuth,
|
||||
HttpServicePreboot,
|
||||
InternalHttpServicePreboot,
|
||||
HttpServiceSetup,
|
||||
InternalHttpServiceSetup,
|
||||
HttpServiceStart,
|
||||
|
|
|
@ -43,6 +43,7 @@ describe('http service', () => {
|
|||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot({ plugins: { initialize: false } });
|
||||
await root.preboot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -189,6 +190,7 @@ describe('http service', () => {
|
|||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
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 () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ describe('http auth', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot({ plugins: { initialize: false } });
|
||||
await root.preboot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
146
src/core/server/http/integration_tests/preboot.test.ts
Normal file
146
src/core/server/http/integration_tests/preboot.test.ts
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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<HttpServiceSetup, 'createRouter' | 'registerRouteHandlerContext'> {
|
||||
|
@ -303,7 +401,7 @@ export interface InternalHttpServiceSetup
|
|||
contextName: ContextName,
|
||||
provider: RequestHandlerContextProvider<Context, ContextName>
|
||||
) => RequestHandlerContextContainer;
|
||||
notReadyServer?: InternalNotReadyHttpServiceSetup;
|
||||
registerPrebootRoutes(path: string, callback: (router: IRouter) => void): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
|
|
@ -13,12 +13,16 @@ const createHttpResourcesMock = (): jest.Mocked<HttpResources> => ({
|
|||
register: jest.fn(),
|
||||
});
|
||||
|
||||
function createInternalHttpResourcesSetup() {
|
||||
function createInternalHttpResourcesPreboot() {
|
||||
return {
|
||||
createRegistrar: jest.fn(() => createHttpResourcesMock()),
|
||||
};
|
||||
}
|
||||
|
||||
function createInternalHttpResourcesSetup() {
|
||||
return createInternalHttpResourcesPreboot();
|
||||
}
|
||||
|
||||
function createHttpResourcesResponseFactory() {
|
||||
const mocked: jest.Mocked<HttpResourcesServiceToolkit> = {
|
||||
renderCoreApp: jest.fn(),
|
||||
|
@ -35,6 +39,7 @@ function createHttpResourcesResponseFactory() {
|
|||
|
||||
export const httpResourcesMock = {
|
||||
createRegistrar: createHttpResourcesMock,
|
||||
createPrebootContract: createInternalHttpResourcesPreboot,
|
||||
createSetupContract: createInternalHttpResourcesSetup,
|
||||
createResponseFactory: createHttpResourcesResponseFactory,
|
||||
};
|
||||
|
|
|
@ -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<IRouter>;
|
||||
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<any, any, any, 'get'> = { 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<HttpResources['register']>,
|
||||
getDeps: () => PrebootDeps | SetupDeps
|
||||
) {
|
||||
describe(`${name} register`, () => {
|
||||
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
|
||||
let register: HttpResources['register'];
|
||||
beforeEach(async () => {
|
||||
register = await initializer();
|
||||
});
|
||||
|
||||
it('can attach headers, except the CSP header', async () => {
|
||||
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
|
||||
const { createRegistrar } = await service.setup(setupDeps);
|
||||
const { register } = createRegistrar(router);
|
||||
register(routeConfig, async (ctx, req, res) => {
|
||||
return res.renderCoreApp({
|
||||
describe('renderCoreApp', () => {
|
||||
it('formats successful response', async () => {
|
||||
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(getDeps().rendering.render).toHaveBeenCalledWith(
|
||||
kibanaRequest,
|
||||
context.core.uiSettings.client,
|
||||
{
|
||||
includeUserSettings: true,
|
||||
vars: {
|
||||
apmConfig,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const [[, routeHandler]] = router.get.mock.calls;
|
||||
|
||||
const responseFactory = httpResourcesMock.createResponseFactory();
|
||||
await routeHandler(context, kibanaRequest, responseFactory);
|
||||
|
||||
expect(responseFactory.ok).toHaveBeenCalledWith({
|
||||
body: '<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'",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const [[, routeHandler]] = router.get.mock.calls;
|
||||
|
||||
const responseFactory = httpResourcesMock.createResponseFactory();
|
||||
await routeHandler(context, kibanaRequest, responseFactory);
|
||||
|
||||
expect(responseFactory.ok).toHaveBeenCalledWith({
|
||||
body: '<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<any, any, any, 'get'> = { path: '/', validate: false };
|
||||
const { createRegistrar } = await service.setup(setupDeps);
|
||||
const { register } = createRegistrar(router);
|
||||
register(routeConfig, async (ctx, req, res) => {
|
||||
return res.renderAnonymousCoreApp();
|
||||
describe('renderAnonymousCoreApp', () => {
|
||||
it('formats successful response', async () => {
|
||||
register(routeConfig, async (ctx, req, res) => {
|
||||
return res.renderAnonymousCoreApp();
|
||||
});
|
||||
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 [[, 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,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
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',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can attach headers, except the CSP header', async () => {
|
||||
const routeConfig: RouteConfig<any, any, any, 'get'> = { 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 responseFactory = httpResourcesMock.createResponseFactory();
|
||||
await routeHandler(context, kibanaRequest, responseFactory);
|
||||
|
||||
expect(responseFactory.ok).toHaveBeenCalledWith({
|
||||
body: '<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'",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const [[, routeHandler]] = router.get.mock.calls;
|
||||
|
||||
const responseFactory = httpResourcesMock.createResponseFactory();
|
||||
await routeHandler(context, kibanaRequest, responseFactory);
|
||||
|
||||
expect(responseFactory.ok).toHaveBeenCalledWith({
|
||||
body: '<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 = '<html><body /></html>';
|
||||
const routeConfig: RouteConfig<any, any, any, 'get'> = { 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;
|
||||
describe('renderHtml', () => {
|
||||
it('formats successful response', async () => {
|
||||
const htmlBody = '<html><body /></html>';
|
||||
register(routeConfig, async (ctx, req, res) => {
|
||||
return res.renderHtml({ body: htmlBody });
|
||||
});
|
||||
const [[, routeHandler]] = router.get.mock.calls;
|
||||
|
||||
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'",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can attach headers, except the CSP & "content-type" headers', async () => {
|
||||
const htmlBody = '<html><body /></html>';
|
||||
const routeConfig: RouteConfig<any, any, any, 'get'> = { 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 = '<html><body /></html>';
|
||||
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<any, any, any, 'get'> = { 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'",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can attach headers, except the CSP & "content-type" headers', async () => {
|
||||
const jsBody = 'alert(1);';
|
||||
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
|
||||
const { createRegistrar } = await service.setup(setupDeps);
|
||||
const { register } = createRegistrar(router);
|
||||
register(routeConfig, async (ctx, req, res) => {
|
||||
return res.renderJs({
|
||||
body: jsBody,
|
||||
expect(responseFactory.ok).toHaveBeenCalledWith({
|
||||
body: htmlBody,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
'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('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;
|
||||
|
||||
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'",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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 responseFactory = httpResourcesMock.createResponseFactory();
|
||||
await routeHandler(context, kibanaRequest, responseFactory);
|
||||
const [[, routeHandler]] = router.get.mock.calls;
|
||||
|
||||
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'",
|
||||
},
|
||||
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'",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<InternalHttpResourcesSe
|
|||
this.logger = core.logger.get('http-resources');
|
||||
}
|
||||
|
||||
preboot(deps: PrebootDeps) {
|
||||
this.logger.debug('prebooting HttpResourcesService');
|
||||
return {
|
||||
createRegistrar: this.createRegistrar.bind(this, deps),
|
||||
};
|
||||
}
|
||||
|
||||
setup(deps: SetupDeps) {
|
||||
this.logger.debug('setting up HttpResourcesService');
|
||||
return {
|
||||
|
@ -54,7 +67,7 @@ export class HttpResourcesService implements CoreService<InternalHttpResourcesSe
|
|||
|
||||
stop() {}
|
||||
|
||||
private createRegistrar(deps: SetupDeps, router: IRouter): HttpResources {
|
||||
private createRegistrar(deps: SetupDeps | PrebootDeps, router: IRouter): HttpResources {
|
||||
return {
|
||||
register: <P, Q, B, Context extends RequestHandlerContext = RequestHandlerContext>(
|
||||
route: RouteConfig<P, Q, B, 'get'>,
|
||||
|
@ -71,7 +84,7 @@ export class HttpResourcesService implements CoreService<InternalHttpResourcesSe
|
|||
}
|
||||
|
||||
private createResponseToolkit(
|
||||
deps: SetupDeps,
|
||||
deps: SetupDeps | PrebootDeps,
|
||||
context: RequestHandlerContext,
|
||||
request: KibanaRequest,
|
||||
response: KibanaResponseFactory
|
||||
|
|
|
@ -14,5 +14,6 @@ export type {
|
|||
HttpResourcesServiceToolkit,
|
||||
HttpResourcesRequestHandler,
|
||||
HttpResources,
|
||||
InternalHttpResourcesPreboot,
|
||||
InternalHttpResourcesSetup,
|
||||
} from './types';
|
||||
|
|
|
@ -20,6 +20,7 @@ describe('http resources service', () => {
|
|||
},
|
||||
plugins: { initialize: false },
|
||||
});
|
||||
await root.preboot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -25,6 +25,7 @@ type I18nServiceContract = PublicMethodsOf<I18nService>;
|
|||
|
||||
const createMock = () => {
|
||||
const mock: jest.Mocked<I18nServiceContract> = {
|
||||
preboot: jest.fn(),
|
||||
setup: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -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<typeof configServiceMock.create>;
|
||||
let http: ReturnType<typeof httpServiceMock.createInternalSetupContract>;
|
||||
let httpPreboot: ReturnType<typeof httpServiceMock.createInternalPrebootContract>;
|
||||
let httpSetup: ReturnType<typeof httpServiceMock.createInternalSetupContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -44,15 +45,19 @@ 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('#setup', () => {
|
||||
describe('#preboot', () => {
|
||||
it('calls `getKibanaTranslationFiles` with the correct parameters', async () => {
|
||||
getKibanaTranslationFilesMock.mockResolvedValue([]);
|
||||
|
||||
const pluginPaths = ['/pathA', '/pathB'];
|
||||
await service.setup({ pluginPaths, http });
|
||||
await service.preboot({ pluginPaths, http: httpPreboot });
|
||||
|
||||
expect(getKibanaTranslationFilesMock).toHaveBeenCalledTimes(1);
|
||||
expect(getKibanaTranslationFilesMock).toHaveBeenCalledWith('en', pluginPaths);
|
||||
|
@ -62,14 +67,55 @@ describe('I18nService', () => {
|
|||
const translationFiles = ['/path/to/file', 'path/to/another/file'];
|
||||
getKibanaTranslationFilesMock.mockResolvedValue(translationFiles);
|
||||
|
||||
await service.setup({ pluginPaths: [], http });
|
||||
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.setup({ pluginPaths: [], http });
|
||||
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: httpSetup });
|
||||
|
||||
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.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: 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);
|
||||
|
|
|
@ -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<I18nServiceSetup> {
|
||||
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<I18nConfigType>(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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -125,6 +129,7 @@ export type {
|
|||
LegacyElasticsearchClientConfig,
|
||||
LegacyElasticsearchError,
|
||||
LegacyElasticsearchErrorHelpers,
|
||||
ElasticsearchServicePreboot,
|
||||
ElasticsearchServiceSetup,
|
||||
ElasticsearchServiceStart,
|
||||
ElasticsearchStatusMeta,
|
||||
|
@ -143,6 +148,7 @@ export type {
|
|||
ShardsResponse,
|
||||
GetResponse,
|
||||
DeleteDocumentResponse,
|
||||
ElasticsearchConfigPreboot,
|
||||
} from './elasticsearch';
|
||||
|
||||
export type {
|
||||
|
@ -179,6 +185,7 @@ export type {
|
|||
HttpResponseOptions,
|
||||
HttpResponsePayload,
|
||||
HttpServerInfo,
|
||||
HttpServicePreboot,
|
||||
HttpServiceSetup,
|
||||
HttpServiceStart,
|
||||
ErrorHttpResponseOptions,
|
||||
|
@ -260,8 +267,11 @@ export type {
|
|||
AppenderConfigType,
|
||||
} from './logging';
|
||||
|
||||
export { PluginType } from './plugins';
|
||||
|
||||
export type {
|
||||
DiscoveredPlugin,
|
||||
PrebootPlugin,
|
||||
Plugin,
|
||||
AsyncPlugin,
|
||||
PluginConfigDescriptor,
|
||||
|
@ -468,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`.
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue