Introduce preboot lifecycle stage (#103636)

This commit is contained in:
Aleh Zasypkin 2021-07-20 06:52:23 +02:00 committed by GitHub
parent a34a15ea3d
commit 237256a4ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
194 changed files with 5865 additions and 1209 deletions

10
.github/CODEOWNERS vendored
View file

@ -249,14 +249,14 @@
#CC# /x-pack/plugins/translations/ @elastic/kibana-localization @elastic/kibana-core
# Security
/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core
/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core
/src/plugins/security_oss/ @elastic/kibana-security
/src/plugins/spaces_oss/ @elastic/kibana-security
/src/plugins/user_setup/ @elastic/kibana-security
/test/security_functional/ @elastic/kibana-security
/x-pack/plugins/spaces/ @elastic/kibana-security
/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security
/x-pack/plugins/security/ @elastic/kibana-security
/x-pack/plugins/spaces/ @elastic/kibana-security
/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security
/x-pack/plugins/security/ @elastic/kibana-security
/x-pack/test/api_integration/apis/security/ @elastic/kibana-security
/x-pack/test/ui_capabilities/ @elastic/kibana-security
/x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security
@ -265,6 +265,8 @@
/x-pack/test/security_functional/ @elastic/kibana-security
/x-pack/test/spaces_api_integration/ @elastic/kibana-security
/x-pack/test/saved_object_api_integration/ @elastic/kibana-security
/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core
/examples/preboot_example/ @elastic/kibana-security @elastic/kibana-core
#CC# /x-pack/plugins/security/ @elastic/kibana-security
# Kibana Alerting Services

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [CorePreboot](./kibana-plugin-core-server.corepreboot.md) &gt; [elasticsearch](./kibana-plugin-core-server.corepreboot.elasticsearch.md)
## CorePreboot.elasticsearch property
[ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md)
<b>Signature:</b>
```typescript
elasticsearch: ElasticsearchServicePreboot;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [CorePreboot](./kibana-plugin-core-server.corepreboot.md) &gt; [http](./kibana-plugin-core-server.corepreboot.http.md)
## CorePreboot.http property
[HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md)
<b>Signature:</b>
```typescript
http: HttpServicePreboot;
```

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [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) |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [CorePreboot](./kibana-plugin-core-server.corepreboot.md) &gt; [preboot](./kibana-plugin-core-server.corepreboot.preboot.md)
## CorePreboot.preboot property
[PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md)
<b>Signature:</b>
```typescript
preboot: PrebootServicePreboot;
```

View file

@ -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>

View file

@ -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>. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) &gt; [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;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ElasticsearchConfigPreboot](./kibana-plugin-core-server.elasticsearchconfigpreboot.md) &gt; [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;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ElasticsearchConfigPreboot](./kibana-plugin-core-server.elasticsearchconfigpreboot.md) &gt; [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[];
```

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [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. |

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) &gt; [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;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) &gt; [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();
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [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&lt;ElasticsearchConfigPreboot&gt;</code> | A limited set of Elasticsearch configuration entries. |
| [createClient](./kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md) | <code>(type: string, clientConfig?: Partial&lt;ElasticsearchClientConfig&gt;) =&gt; ICustomClusterClient</code> | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md)<!-- -->. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) &gt; [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;
```

View file

@ -0,0 +1,82 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [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. |

View file

@ -0,0 +1,40 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) &gt; [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) =&gt; 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' }));
});
```

View file

@ -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)<!-- -->. |

View file

@ -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>

View file

@ -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>;
```

View file

@ -11,5 +11,6 @@ env: {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
instanceUuid: string;
configs: readonly string[];
};
```

View file

@ -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&lt;SharedGlobalConfig&gt;;</code><br/><code> get: () =&gt; SharedGlobalConfig;</code><br/><code> };</code><br/><code> create: &lt;T = ConfigSchema&gt;() =&gt; Observable&lt;T&gt;;</code><br/><code> get: &lt;T = ConfigSchema&gt;() =&gt; 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&lt;PackageInfo&gt;;</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&lt;PackageInfo&gt;;</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> | |

View file

@ -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. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) &gt; [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;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PluginType](./kibana-plugin-core-server.plugintype.md)
## PluginType enum
<b>Signature:</b>
```typescript
export declare enum PluginType
```
## Enumeration Members
| Member | Value | Description |
| --- | --- | --- |
| preboot | <code>&quot;preboot&quot;</code> | Preboot plugins are special-purpose plugins that only function during preboot stage. |
| standard | <code>&quot;standard&quot;</code> | Standard plugins are plugins that start to function as soon as Kibana is fully booted and are active until it shuts down. |

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [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) | |

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PrebootPlugin](./kibana-plugin-core-server.prebootplugin.md) &gt; [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`

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PrebootPlugin](./kibana-plugin-core-server.prebootplugin.md) &gt; [stop](./kibana-plugin-core-server.prebootplugin.stop.md)
## PrebootPlugin.stop() method
<b>Signature:</b>
```typescript
stop?(): void;
```
<b>Returns:</b>
`void`

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) &gt; [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;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) &gt; [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;
```

View file

@ -0,0 +1,45 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [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&lt;{</code><br/><code> shouldReloadConfig: boolean;</code><br/><code> } &#124; undefined&gt;) =&gt; 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>() =&gt; boolean</code> | Indicates whether Kibana is currently on hold and cannot proceed to <code>setup</code> yet. |

View file

@ -0,0 +1,3 @@
# `prebootExample` plugin
The example of the `preboot` plugin.

View 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": []
}

View 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>
);
};

View 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;
}

View 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);

View 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) {}
}

View 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 }),
});

View 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);

View 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() {}
}

View 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" }]
}

View file

@ -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' });

View file

@ -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;
}

View file

@ -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';

View file

@ -337,6 +337,7 @@ describe('myPlugin', () => {
let root: ReturnType<typeof kbnTestServer.createRoot>;
beforeAll(async () => {
root = kbnTestServer.createRoot();
await root.preboot();
await root.setup();
await root.start();
}, 30000);
@ -382,6 +383,7 @@ describe('myPlugin', () => {
let root: ReturnType<typeof kbnTestServer.createRoot>;
beforeAll(async () => {
root = kbnTestServer.createRoot();
await root.preboot();
await root.setup();
await root.start();
}, 30000);

View file

@ -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,

View file

@ -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,

View file

@ -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) {

View file

@ -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()),
};

View file

@ -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 });

View file

@ -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) => {

View file

@ -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();
});

View file

@ -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);
}

View file

@ -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 () => {

View file

@ -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);
}

View file

@ -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);

View file

@ -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,
};

View file

@ -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);

View file

@ -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}
*

View file

@ -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,

View file

@ -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);

View file

@ -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')
);

View file

@ -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 () => {

View file

@ -20,6 +20,7 @@ describe('Core app routes', () => {
},
});
await root.preboot();
await root.setup();
await root.start();
});

View file

@ -24,6 +24,7 @@ describe('default route provider', () => {
},
});
await root.preboot();
await root.setup();
await root.start();
});

View file

@ -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();
});

View file

@ -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,

View file

@ -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]`
);
});

View file

@ -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'),

View file

@ -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 {

View file

@ -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;
}

View file

@ -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,
};

View file

@ -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' });
});
});
});

View file

@ -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,
};
}
}

View file

@ -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';

View file

@ -41,6 +41,7 @@ describe('trace', () => {
},
},
});
await root.preboot();
}, 30000);
afterEach(async () => {
@ -167,6 +168,7 @@ describe('trace', () => {
},
},
});
await rootExecutionContextDisabled.preboot();
}, 30000);
afterEach(async () => {

View file

@ -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 [

View file

@ -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,

View file

@ -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();

View file

@ -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;
}
}

View file

@ -87,6 +87,8 @@ export type {
RequestHandlerContextContainer,
RequestHandlerContextProvider,
HttpAuth,
HttpServicePreboot,
InternalHttpServicePreboot,
HttpServiceSetup,
InternalHttpServiceSetup,
HttpServiceStart,

View file

@ -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 () => {

View file

@ -15,6 +15,7 @@ describe('http auth', () => {
beforeEach(async () => {
root = kbnTestServer.createRoot({ plugins: { initialize: false } });
await root.preboot();
}, 30000);
afterEach(async () => {

View file

@ -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 () => {

View file

@ -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;

View file

@ -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

View 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');
});
});

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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 */

View file

@ -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,
};

View file

@ -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
);
});
});

View file

@ -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

View file

@ -14,5 +14,6 @@ export type {
HttpResourcesServiceToolkit,
HttpResourcesRequestHandler,
HttpResources,
InternalHttpResourcesPreboot,
InternalHttpResourcesSetup,
} from './types';

View file

@ -20,6 +20,7 @@ describe('http resources service', () => {
},
plugins: { initialize: false },
});
await root.preboot();
}, 30000);
afterEach(async () => {

View file

@ -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:

View file

@ -25,6 +25,7 @@ type I18nServiceContract = PublicMethodsOf<I18nService>;
const createMock = () => {
const mock: jest.Mocked<I18nServiceContract> = {
preboot: jest.fn(),
setup: jest.fn(),
};

View file

@ -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);

View file

@ -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 };
}
}

View file

@ -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