Migrate Index Management and enrichers to the new ES JS client (#105863)

* Destructure index API request bodies consistently.
* Remove unnecessary calls to encodeURIComponent on the server.
* Migrate routes to handleEsError. Delete wrapEsError helpers. Remove unused isEsError and parseEsError dependencies. Remove isEsError from es_ui_shared.
* Update tests and migrate API integration tests.
* Clarify test details in CCR README. Update Index Management README with steps for testing Cloud-managed index templates and steps for testing indices and data streams that contain special characters.
This commit is contained in:
CJ Cenizal 2021-08-02 12:20:54 -05:00 committed by GitHub
parent 5e8b24230a
commit 3491f05e95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 682 additions and 1135 deletions

View file

@ -457,7 +457,7 @@ Index Management by running this series of requests in Console:
|{kib-repo}blob/{branch}/x-pack/plugins/index_management/README.md[indexManagement]
|Create a data stream using Console and you'll be able to view it in the UI:
|Create an index with special characters and verify it renders correctly:
|{kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra]

View file

@ -6,6 +6,5 @@
* Side Public License, v 1.
*/
export { isEsError } from './is_es_error';
export { handleEsError } from './handle_es_error';
export { parseEsError } from './es_error_parser';

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as legacyElasticsearch from 'elasticsearch';
const esErrorsParent = legacyElasticsearch.errors._Abstract;
interface RequestError extends Error {
statusCode?: number;
}
/*
* @deprecated
* Only works with legacy elasticsearch js client errors and will be removed after 7.x last
*/
export function isEsError(err: RequestError) {
const isInstanceOfEsError = err instanceof esErrorsParent;
const hasStatusCode = Boolean(err.statusCode);
return isInstanceOfEsError && hasStatusCode;
}

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { isEsError, handleEsError, parseEsError } from '../../__packages_do_not_import__/errors';
export { handleEsError, parseEsError } from '../../__packages_do_not_import__/errors';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
export { isEsError, handleEsError, parseEsError } from './errors';
export { handleEsError, parseEsError } from './errors';
/** dummy plugin*/
export function plugin() {

View file

@ -4,14 +4,15 @@
You can run a local cluster and simulate a remote cluster within a single Kibana directory.
1. Ensure Kibana isn't running so it doesn't load up any data into your cluster. Run `yarn es snapshot --license=trial` to install a fresh snapshot. Wait for ES to finish setting up.
1. Ensure Kibana isn't running so it doesn't load up any data into your cluster. Run `yarn es snapshot --license=trial` to install a fresh snapshot. Wait for ES to finish setting up and activate the license.
2. Create a "remote" copy of your ES snapshot by running: `cp -R .es/8.0.0 .es/8.0.0-2`.
4. Start your "remote" cluster by running `.es/8.0.0-2/bin/elasticsearch -E cluster.name=europe -E transport.port=9400`.
4. Run `yarn start` to start Kibana.
5. Index a document into your remote cluster by running `curl -X PUT http://elastic:changeme@localhost:9201/my-leader-index --data '{"settings":{"number_of_shards":1,"soft_deletes.enabled":true}}' --header "Content-Type: application/json"`. Note that these settings are required for testing auto-follow pattern conflicts errors (see below).
4. Start your "local" cluster by running `.es/8.0.0/bin/elasticsearch`.
5. Run `yarn start` to start Kibana so that it connects to the "local" cluster.
6. Start your "remote" cluster by running `.es/8.0.0-2/bin/elasticsearch -E cluster.name=europe -E transport.port=9400`.
7. Index a document into your "remote" cluster by running `curl -X PUT http://elastic:changeme@localhost:9201/my-leader-index --data '{"settings":{"number_of_shards":1,"soft_deletes.enabled":true}}' --header "Content-Type: application/json"`. Note that these settings are required for testing auto-follow pattern conflicts errors (see below).
Now you can create follower indices and auto-follow patterns to replicate the `my-leader-index`
index on the remote cluster that's available at `127.0.0.1:9400`.
index on the "remote" cluster that's available at `127.0.0.1:9400`.
### Auto-follow pattern conflict errors

View file

@ -7,14 +7,8 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import {
CoreSetup,
CoreStart,
Plugin,
Logger,
PluginInitializerContext,
LegacyAPICaller,
} from 'src/core/server';
import { CoreSetup, CoreStart, Plugin, Logger, PluginInitializerContext } from 'src/core/server';
import { IScopedClusterClient } from 'kibana/server';
import { Index } from '../../index_management/server';
import { PLUGIN } from '../common/constants';
@ -23,20 +17,18 @@ import { registerApiRoutes } from './routes';
import { CrossClusterReplicationConfig } from './config';
import { License, handleEsError } from './shared_imports';
// TODO replace deprecated ES client after Index Management is updated
const ccrDataEnricher = async (indicesList: Index[], callWithRequest: LegacyAPICaller) => {
const ccrDataEnricher = async (indicesList: Index[], client: IScopedClusterClient) => {
if (!indicesList?.length) {
return indicesList;
}
const params = {
path: '/_all/_ccr/info',
method: 'GET',
};
try {
const { follower_indices: followerIndices } = await callWithRequest(
'transport.request',
params
);
const {
body: { follower_indices: followerIndices },
} = await client.asCurrentUser.ccr.followInfo({
index: '_all',
});
return indicesList.map((index) => {
const isFollowerIndex = !!followerIndices.find(
(followerIndex: { follower_index: string }) => {

View file

@ -6,44 +6,36 @@
*/
import { i18n } from '@kbn/i18n';
import {
CoreSetup,
Plugin,
Logger,
PluginInitializerContext,
LegacyAPICaller,
} from 'src/core/server';
import { handleEsError } from './shared_imports';
import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'src/core/server';
import { IScopedClusterClient } from 'kibana/server';
import { Index as IndexWithoutIlm } from '../../index_management/common/types';
import { PLUGIN } from '../common/constants';
import { Index, IndexLifecyclePolicy } from '../common/types';
import { Index } from '../common/types';
import { Dependencies } from './types';
import { registerApiRoutes } from './routes';
import { License } from './services';
import { IndexLifecycleManagementConfig } from './config';
import { handleEsError } from './shared_imports';
const indexLifecycleDataEnricher = async (
indicesList: IndexWithoutIlm[],
// TODO replace deprecated ES client after Index Management is updated
callAsCurrentUser: LegacyAPICaller
client: IScopedClusterClient
): Promise<Index[]> => {
if (!indicesList || !indicesList.length) {
return [];
}
const params = {
path: '/*/_ilm/explain',
method: 'GET',
};
const { indices: ilmIndicesData } = await callAsCurrentUser<{
indices: { [indexName: string]: IndexLifecyclePolicy };
}>('transport.request', params);
const {
body: { indices: ilmIndicesData },
} = await client.asCurrentUser.ilm.explainLifecycle({
index: '*',
});
return indicesList.map((index: IndexWithoutIlm) => {
return {
...index,
// @ts-expect-error @elastic/elasticsearch Element implicitly has an 'any' type
ilm: { ...(ilmIndicesData[index.name] || {}) },
};
});

View file

@ -1,5 +1,16 @@
# Index Management UI
## Indices tab
### Quick steps for testing
Create an index with special characters and verify it renders correctly:
```
# Renders as %{[@metadata][beat]}-%{[@metadata][version]}-2020.08.23
PUT %25%7B%5B%40metadata%5D%5Bbeat%5D%7D-%25%7B%5B%40metadata%5D%5Bversion%5D%7D-2020.08.23
```
## Data streams tab
### Quick steps for testing
@ -20,3 +31,55 @@ POST ds/_doc
"@timestamp": "2020-01-27"
}
```
Create a data stream with special characters and verify it renders correctly:
```
# Configure template for creating a data stream
PUT _index_template/special_ds
{
"index_patterns": ["%{[@metadata][beat]}-%{[@metadata][version]}-2020.08.23"],
"data_stream": {}
}
# Add a document to the data stream, which will render as %{[@metadata][beat]}-%{[@metadata][version]}-2020.08.23
POST %25%7B%5B%40metadata%5D%5Bbeat%5D%7D-%25%7B%5B%40metadata%5D%5Bversion%5D%7D-2020.08.23/_doc
{
"@timestamp": "2020-01-27"
}
```
## Index templates tab
### Quick steps for testing
By default, **legacy index templates** are not shown in the UI. Make them appear by creating one in Console:
```
PUT _template/template_1
{
"index_patterns": ["foo*"]
}
```
To test **Cloud-managed templates**:
1. Add `cluster.metadata.managed_index_templates` setting via Dev Tools:
```
PUT /_cluster/settings
{
"persistent": {
"cluster.metadata.managed_index_templates": ".cloud-"
}
}
```
2. Create a template with the format: `.cloud-<template_name>` via Dev Tools.
```
PUT _template/.cloud-example
{
"index_patterns": [ "foobar*"]
}
```
The UI will now prevent you from editing or deleting this template.

View file

@ -6,7 +6,6 @@
*/
import React, { useState, useCallback, useEffect } from 'react';
import uuid from 'uuid';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
@ -37,11 +36,6 @@ export const SimulateTemplate = React.memo(({ template, filters }: Props) => {
}
const indexTemplate = serializeTemplate(stripEmptyFields(template) as TemplateDeserialized);
// Until ES fixes a bug on their side we will send a random index pattern to the simulate API.
// Issue: https://github.com/elastic/elasticsearch/issues/59152
indexTemplate.index_patterns = [uuid.v4()];
const { data, error } = await simulateIndexTemplate(indexTemplate);
let filteredTemplate = data;

View file

@ -1,174 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => {
const ca = components.clientAction.factory;
Client.prototype.dataManagement = components.clientAction.namespaceFactory();
const dataManagement = Client.prototype.dataManagement.prototype;
// Data streams
// We don't allow the user to create a data stream in the UI or API. We're just adding this here
// to enable the API integration tests.
dataManagement.createDataStream = ca({
urls: [
{
fmt: '/_data_stream/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
method: 'PUT',
});
dataManagement.deleteDataStream = ca({
urls: [
{
fmt: '/_data_stream/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
method: 'DELETE',
});
// Component templates
dataManagement.getComponentTemplates = ca({
urls: [
{
fmt: '/_component_template',
},
],
method: 'GET',
});
dataManagement.getComponentTemplate = ca({
urls: [
{
fmt: '/_component_template/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
method: 'GET',
});
dataManagement.saveComponentTemplate = ca({
urls: [
{
fmt: '/_component_template/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
method: 'PUT',
});
dataManagement.deleteComponentTemplate = ca({
urls: [
{
fmt: '/_component_template/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
method: 'DELETE',
});
// Composable index templates
dataManagement.getComposableIndexTemplates = ca({
urls: [
{
fmt: '/_index_template',
},
],
method: 'GET',
});
dataManagement.getComposableIndexTemplate = ca({
urls: [
{
fmt: '/_index_template/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
method: 'GET',
});
dataManagement.saveComposableIndexTemplate = ca({
urls: [
{
fmt: '/_index_template/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
needBody: true,
method: 'PUT',
});
dataManagement.deleteComposableIndexTemplate = ca({
urls: [
{
fmt: '/_index_template/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
method: 'DELETE',
});
dataManagement.existsTemplate = ca({
urls: [
{
fmt: '/_index_template/<%=name%>',
req: {
name: {
type: 'string',
},
},
},
],
method: 'HEAD',
});
dataManagement.simulateTemplate = ca({
urls: [
{
fmt: '/_index_template/_simulate',
},
],
needBody: true,
method: 'POST',
});
};

View file

@ -10,7 +10,7 @@ import { PluginInitializerContext } from 'src/core/server';
import { IndexMgmtServerPlugin } from './plugin';
import { configSchema } from './config';
export const plugin = (ctx: PluginInitializerContext) => new IndexMgmtServerPlugin(ctx);
export const plugin = (context: PluginInitializerContext) => new IndexMgmtServerPlugin(context);
export const config = {
schema: configSchema,

View file

@ -5,99 +5,57 @@
* 2.0.
*/
import { CatIndicesParams } from 'elasticsearch';
import { IScopedClusterClient } from 'kibana/server';
import { IndexDataEnricher } from '../services';
import { CallAsCurrentUser } from '../types';
import { Index } from '../index';
interface Hit {
health: string;
status: string;
index: string;
uuid: string;
pri: string;
rep: string;
'docs.count': any;
'store.size': any;
sth: 'true' | 'false';
hidden: boolean;
}
interface IndexInfo {
aliases: { [aliasName: string]: unknown };
mappings: unknown;
data_stream?: string;
settings: {
index: {
hidden: 'true' | 'false';
};
};
}
interface GetIndicesResponse {
[indexName: string]: IndexInfo;
}
async function fetchIndicesCall(
callAsCurrentUser: CallAsCurrentUser,
client: IScopedClusterClient,
indexNames?: string[]
): Promise<Index[]> {
const indexNamesString = indexNames && indexNames.length ? indexNames.join(',') : '*';
// This call retrieves alias and settings (incl. hidden status) information about indices
const indices: GetIndicesResponse = await callAsCurrentUser('transport.request', {
method: 'GET',
// transport.request doesn't do any URI encoding, unlike other JS client APIs. This enables
// working with Logstash indices with names like %{[@metadata][beat]}-%{[@metadata][version]}.
path: `/${encodeURIComponent(indexNamesString)}`,
query: {
const { body: indices } = await client.asCurrentUser.indices.get({
index: indexNamesString,
expand_wildcards: 'hidden,all',
},
});
if (!Object.keys(indices).length) {
return [];
}
const catQuery: Pick<CatIndicesParams, 'format' | 'h'> & {
expand_wildcards: string;
index?: string;
} = {
const { body: catHits } = await client.asCurrentUser.cat.indices({
format: 'json',
h: 'health,status,index,uuid,pri,rep,docs.count,sth,store.size',
expand_wildcards: 'hidden,all',
index: indexNamesString,
};
// This call retrieves health and other high-level information about indices.
const catHits: Hit[] = await callAsCurrentUser('transport.request', {
method: 'GET',
path: '/_cat/indices',
query: catQuery,
});
// System indices may show up in _cat APIs, as these APIs are primarily used for troubleshooting
// For now, we filter them out and only return index information for the indices we have
// In the future, we should migrate away from using cat APIs (https://github.com/elastic/kibana/issues/57286)
return catHits.reduce((decoratedIndices, hit) => {
const index = indices[hit.index];
const index = indices[hit.index!];
if (typeof index !== 'undefined') {
const aliases = Object.keys(index.aliases);
const aliases = Object.keys(index.aliases!);
decoratedIndices.push({
health: hit.health,
status: hit.status,
name: hit.index,
uuid: hit.uuid,
primary: hit.pri,
replica: hit.rep,
health: hit.health!,
status: hit.status!,
name: hit.index!,
uuid: hit.uuid!,
primary: hit.pri!,
replica: hit.rep!,
documents: hit['docs.count'],
size: hit['store.size'],
isFrozen: hit.sth === 'true', // sth value coming back as a string from ES
aliases: aliases.length ? aliases : 'none',
// @ts-expect-error @elastic/elasticsearch Property 'index' does not exist on type 'IndicesIndexSettings | IndicesIndexStatePrefixedSettings'.
hidden: index.settings.index.hidden === 'true',
data_stream: index.data_stream,
// @ts-expect-error @elastic/elasticsearch Property 'data_stream' does not exist on type 'IndicesIndexState'.
data_stream: index.data_stream!,
});
}
@ -106,10 +64,10 @@ async function fetchIndicesCall(
}
export const fetchIndices = async (
callAsCurrentUser: CallAsCurrentUser,
client: IScopedClusterClient,
indexDataEnricher: IndexDataEnricher,
indexNames?: string[]
) => {
const indices = await fetchIndicesCall(callAsCurrentUser, indexNames);
return await indexDataEnricher.enrichIndices(indices, callAsCurrentUser);
const indices = await fetchIndicesCall(client, indexNames);
return await indexDataEnricher.enrichIndices(indices, client);
};

View file

@ -5,16 +5,20 @@
* 2.0.
*/
import { IScopedClusterClient } from 'kibana/server';
// Cloud has its own system for managing templates and we want to make
// this clear in the UI when a template is used in a Cloud deployment.
export const getCloudManagedTemplatePrefix = async (
callAsCurrentUser: any
client: IScopedClusterClient
): Promise<string | undefined> => {
try {
const { persistent, transient, defaults } = await callAsCurrentUser('cluster.getSettings', {
filterPath: '*.*managed_index_templates',
flatSettings: true,
includeDefaults: true,
const {
body: { persistent, transient, defaults },
} = await client.asCurrentUser.cluster.getSettings({
filter_path: '*.*managed_index_templates',
flat_settings: true,
include_defaults: true,
});
const { 'cluster.metadata.managed_index_templates': managedTemplatesPrefix = undefined } = {

View file

@ -5,20 +5,13 @@
* 2.0.
*/
import {
CoreSetup,
Plugin,
PluginInitializerContext,
ILegacyCustomClusterClient,
} from 'src/core/server';
import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server';
import { PLUGIN } from '../common/constants/plugin';
import { Dependencies } from './types';
import { ApiRoutes } from './routes';
import { IndexDataEnricher } from './services';
import { isEsError, handleEsError, parseEsError } from './shared_imports';
import { elasticsearchJsPlugin } from './client/elasticsearch';
import type { IndexManagementRequestHandlerContext } from './types';
import { handleEsError } from './shared_imports';
export interface IndexManagementPluginSetup {
indexDataEnricher: {
@ -26,16 +19,9 @@ export interface IndexManagementPluginSetup {
};
}
async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) {
const [core] = await getStartServices();
const esClientConfig = { plugins: [elasticsearchJsPlugin] };
return core.elasticsearch.legacy.createClient('dataManagement', esClientConfig);
}
export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup, void, any, any> {
private readonly apiRoutes: ApiRoutes;
private readonly indexDataEnricher: IndexDataEnricher;
private dataManagementESClient?: ILegacyCustomClusterClient;
constructor(initContext: PluginInitializerContext) {
this.apiRoutes = new ApiRoutes();
@ -46,8 +32,6 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
{ http, getStartServices }: CoreSetup,
{ features, security }: Dependencies
): IndexManagementPluginSetup {
const router = http.createRouter<IndexManagementRequestHandlerContext>();
features.registerElasticsearchFeature({
id: PLUGIN.id,
management: {
@ -63,27 +47,13 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
],
});
http.registerRouteHandlerContext<IndexManagementRequestHandlerContext, 'dataManagement'>(
'dataManagement',
async (ctx, request) => {
this.dataManagementESClient =
this.dataManagementESClient ?? (await getCustomEsClient(getStartServices));
return {
client: this.dataManagementESClient.asScoped(request),
};
}
);
this.apiRoutes.setup({
router,
router: http.createRouter(),
config: {
isSecurityEnabled: () => security !== undefined && security.license.isEnabled(),
},
indexDataEnricher: this.indexDataEnricher,
lib: {
isEsError,
parseEsError,
handleEsError,
},
});
@ -97,9 +67,5 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
start() {}
stop() {
if (this.dataManagementESClient) {
this.dataManagementESClient.close();
}
}
stop() {}
}

View file

@ -12,7 +12,10 @@ import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
import { componentTemplateSchema } from './schema_validation';
export const registerCreateRoute = ({ router, lib: { isEsError } }: RouteDependencies): void => {
export const registerCreateRoute = ({
router,
lib: { handleEsError },
}: RouteDependencies): void => {
router.post(
{
path: addBasePath('/component_templates'),
@ -20,24 +23,23 @@ export const registerCreateRoute = ({ router, lib: { isEsError } }: RouteDepende
body: componentTemplateSchema,
},
},
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const serializedComponentTemplate = serializeComponentTemplate(req.body);
const serializedComponentTemplate = serializeComponentTemplate(request.body);
const { name } = req.body;
const { name } = request.body;
try {
// Check that a component template with the same name doesn't already exist
const componentTemplateResponse = await callAsCurrentUser(
'dataManagement.getComponentTemplate',
{ name }
);
const { component_templates: componentTemplates } = componentTemplateResponse;
const {
body: { component_templates: componentTemplates },
} = await client.asCurrentUser.cluster.getComponentTemplate({
name,
});
if (componentTemplates.length) {
return res.conflict({
return response.conflict({
body: new Error(
i18n.translate('xpack.idxMgmt.componentTemplates.createRoute.duplicateErrorMessage', {
defaultMessage: "There is already a component template with name '{name}'.",
@ -53,21 +55,15 @@ export const registerCreateRoute = ({ router, lib: { isEsError } }: RouteDepende
}
try {
const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', {
const { body: responseBody } = await client.asCurrentUser.cluster.putComponentTemplate({
name,
// @ts-expect-error @elastic/elasticsearch Type 'ComponentTemplateSerialized' is not assignable
body: serializedComponentTemplate,
});
return res.ok({ body: response });
return response.ok({ body: responseBody });
} catch (error) {
if (isEsError(error)) {
return res.customError({
statusCode: error.statusCode,
body: error,
});
}
throw error;
return handleEsError({ error, response });
}
}
);

View file

@ -14,7 +14,10 @@ const paramsSchema = schema.object({
names: schema.string(),
});
export const registerDeleteRoute = ({ router }: RouteDependencies): void => {
export const registerDeleteRoute = ({
router,
lib: { handleEsError },
}: RouteDependencies): void => {
router.delete(
{
path: addBasePath('/component_templates/{names}'),
@ -22,32 +25,34 @@ export const registerDeleteRoute = ({ router }: RouteDependencies): void => {
params: paramsSchema,
},
},
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const { names } = req.params;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { names } = request.params;
const componentNames = names.split(',');
const response: { itemsDeleted: string[]; errors: any[] } = {
const responseBody: { itemsDeleted: string[]; errors: any[] } = {
itemsDeleted: [],
errors: [],
};
await Promise.all(
componentNames.map((componentName) => {
return callAsCurrentUser('dataManagement.deleteComponentTemplate', {
componentNames.map(async (componentName) => {
try {
await client.asCurrentUser.cluster.deleteComponentTemplate({
name: componentName,
})
.then(() => response.itemsDeleted.push(componentName))
.catch((e) =>
response.errors.push({
});
return responseBody.itemsDeleted.push(componentName);
} catch (error) {
return responseBody.errors.push({
name: componentName,
error: e,
})
);
error: handleEsError({ error, response }),
});
}
})
);
return res.ok({ body: response });
return response.ok({ body: responseBody });
}
);
};

View file

@ -19,25 +19,23 @@ const paramsSchema = schema.object({
name: schema.string(),
});
export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) {
export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDependencies) {
// Get all component templates
router.get(
{ path: addBasePath('/component_templates'), validate: false },
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
try {
const {
component_templates: componentTemplates,
}: { component_templates: ComponentTemplateFromEs[] } = await callAsCurrentUser(
'dataManagement.getComponentTemplates'
);
body: { component_templates: componentTemplates },
} = await client.asCurrentUser.cluster.getComponentTemplate();
const { index_templates: indexTemplates } = await callAsCurrentUser(
'dataManagement.getComposableIndexTemplates'
);
const {
body: { index_templates: indexTemplates },
} = await client.asCurrentUser.indices.getIndexTemplate();
const body = componentTemplates.map((componentTemplate) => {
const body = componentTemplates.map((componentTemplate: ComponentTemplateFromEs) => {
const deserializedComponentTemplateListItem = deserializeComponentTemplateList(
componentTemplate,
indexTemplates
@ -45,16 +43,9 @@ export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDepende
return deserializedComponentTemplateListItem;
});
return res.ok({ body });
return response.ok({ body });
} catch (error) {
if (isEsError(error)) {
return res.customError({
statusCode: error.statusCode,
body: error,
});
}
throw error;
return handleEsError({ error, response });
}
}
);
@ -67,34 +58,26 @@ export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDepende
params: paramsSchema,
},
},
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const { name } = req.params;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { name } = request.params;
try {
const { component_templates: componentTemplates } = await callAsCurrentUser(
'dataManagement.getComponentTemplates',
{
const {
body: { component_templates: componentTemplates },
} = await client.asCurrentUser.cluster.getComponentTemplate({
name,
}
);
});
const { index_templates: indexTemplates } = await callAsCurrentUser(
'dataManagement.getComposableIndexTemplates'
);
const {
body: { index_templates: indexTemplates },
} = await client.asCurrentUser.indices.getIndexTemplate();
return res.ok({
return response.ok({
body: deserializeComponentTemplate(componentTemplates[0], indexTemplates),
});
} catch (error) {
if (isEsError(error)) {
return res.customError({
statusCode: error.statusCode,
body: error,
});
}
throw error;
return handleEsError({ error, response });
}
}
);

View file

@ -18,17 +18,15 @@ const httpService = httpServiceMock.createSetupContract();
const mockedIndexDataEnricher = new IndexDataEnricher();
const mockRouteContext = ({
callAsCurrentUser,
}: {
callAsCurrentUser: any;
}): RequestHandlerContext => {
const mockRouteContext = ({ hasPrivileges }: { hasPrivileges: unknown }): RequestHandlerContext => {
const routeContextMock = ({
core: {
elasticsearch: {
legacy: {
client: {
callAsCurrentUser,
asCurrentUser: {
security: {
hasPrivileges,
},
},
},
},
@ -51,8 +49,6 @@ describe('GET privileges', () => {
},
indexDataEnricher: mockedIndexDataEnricher,
lib: {
isEsError: jest.fn(),
parseEsError: jest.fn(),
handleEsError: jest.fn(),
},
});
@ -62,15 +58,17 @@ describe('GET privileges', () => {
it('should return the correct response when a user has privileges', async () => {
const privilegesResponseMock = {
body: {
username: 'elastic',
has_all_requested: true,
cluster: { manage_index_templates: true },
index: {},
application: {},
},
};
const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce(privilegesResponseMock),
hasPrivileges: jest.fn().mockResolvedValueOnce(privilegesResponseMock),
});
const request = httpServerMock.createKibanaRequest();
@ -86,15 +84,17 @@ describe('GET privileges', () => {
it('should return the correct response when a user does not have privileges', async () => {
const privilegesResponseMock = {
body: {
username: 'elastic',
has_all_requested: false,
cluster: { manage_index_templates: false },
index: {},
application: {},
},
};
const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn().mockResolvedValueOnce(privilegesResponseMock),
hasPrivileges: jest.fn().mockResolvedValueOnce(privilegesResponseMock),
});
const request = httpServerMock.createKibanaRequest();
@ -119,8 +119,6 @@ describe('GET privileges', () => {
},
indexDataEnricher: mockedIndexDataEnricher,
lib: {
isEsError: jest.fn(),
parseEsError: jest.fn(),
handleEsError: jest.fn(),
},
});
@ -130,7 +128,7 @@ describe('GET privileges', () => {
it('should return the default privileges response', async () => {
const routeContextMock = mockRouteContext({
callAsCurrentUser: jest.fn(),
hasPrivileges: jest.fn(),
});
const request = httpServerMock.createKibanaRequest();

View file

@ -17,13 +17,17 @@ const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } =
return privileges;
}, []);
export const registerPrivilegesRoute = ({ router, config }: RouteDependencies) => {
export const registerPrivilegesRoute = ({
router,
config,
lib: { handleEsError },
}: RouteDependencies) => {
router.get(
{
path: addBasePath('/component_templates/privileges'),
validate: false,
},
async (ctx, req, res) => {
async (context, request, response) => {
const privilegesResult: Privileges = {
hasAllPrivileges: true,
missingPrivileges: {
@ -33,38 +37,28 @@ export const registerPrivilegesRoute = ({ router, config }: RouteDependencies) =
// Skip the privileges check if security is not enabled
if (!config.isSecurityEnabled()) {
return res.ok({ body: privilegesResult });
return response.ok({ body: privilegesResult });
}
const {
core: {
elasticsearch: {
legacy: { client },
},
},
} = ctx;
const { client } = context.core.elasticsearch;
try {
const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser(
'transport.request',
{
path: '/_security/user/_has_privileges',
method: 'POST',
const {
body: { has_all_requested: hasAllPrivileges, cluster },
} = await client.asCurrentUser.security.hasPrivileges({
body: {
cluster: ['manage_index_templates'],
},
}
);
});
if (!hasAllPrivileges) {
privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster);
}
privilegesResult.hasAllPrivileges = hasAllPrivileges;
return res.ok({ body: privilegesResult });
} catch (e) {
throw e;
return response.ok({ body: privilegesResult });
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -15,7 +15,10 @@ const paramsSchema = schema.object({
name: schema.string(),
});
export const registerUpdateRoute = ({ router, lib: { isEsError } }: RouteDependencies): void => {
export const registerUpdateRoute = ({
router,
lib: { handleEsError },
}: RouteDependencies): void => {
router.put(
{
path: addBasePath('/component_templates/{name}'),
@ -24,34 +27,28 @@ export const registerUpdateRoute = ({ router, lib: { isEsError } }: RouteDepende
params: paramsSchema,
},
},
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const { name } = req.params;
const { template, version, _meta } = req.body;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { name } = request.params;
const { template, version, _meta } = request.body;
try {
// Verify component exists; ES will throw 404 if not
await callAsCurrentUser('dataManagement.getComponentTemplate', { name });
await client.asCurrentUser.cluster.getComponentTemplate({ name });
const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', {
const { body: responseBody } = await client.asCurrentUser.cluster.putComponentTemplate({
name,
body: {
// @ts-expect-error @elastic/elasticsearch Not assignable to type 'IndicesIndexState'
template,
version,
_meta,
},
});
return res.ok({ body: response });
return response.ok({ body: responseBody });
} catch (error) {
if (isEsError(error)) {
return res.customError({
statusCode: error.statusCode,
body: error,
});
}
throw error;
return handleEsError({ error, response });
}
}
);

View file

@ -9,23 +9,22 @@ import { schema, TypeOf } from '@kbn/config-schema';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
import { wrapEsError } from '../../helpers';
const bodySchema = schema.object({
dataStreams: schema.arrayOf(schema.string()),
});
export function registerDeleteRoute({ router }: RouteDependencies) {
export function registerDeleteRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{
path: addBasePath('/delete_data_streams'),
validate: { body: bodySchema },
},
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const { dataStreams } = req.body as TypeOf<typeof bodySchema>;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { dataStreams } = request.body as TypeOf<typeof bodySchema>;
const response: { dataStreamsDeleted: string[]; errors: any[] } = {
const responseBody: { dataStreamsDeleted: string[]; errors: any[] } = {
dataStreamsDeleted: [],
errors: [],
};
@ -33,21 +32,21 @@ export function registerDeleteRoute({ router }: RouteDependencies) {
await Promise.all(
dataStreams.map(async (name: string) => {
try {
await callAsCurrentUser('dataManagement.deleteDataStream', {
await client.asCurrentUser.indices.deleteDataStream({
name,
});
return response.dataStreamsDeleted.push(name);
} catch (e) {
return response.errors.push({
return responseBody.dataStreamsDeleted.push(name);
} catch (error) {
return responseBody.errors.push({
name,
error: wrapEsError(e),
error: handleEsError({ error, response }),
});
}
})
);
return res.ok({ body: response });
return response.ok({ body: responseBody });
}
);
}

View file

@ -7,7 +7,7 @@
import { schema, TypeOf } from '@kbn/config-schema';
import { ElasticsearchClient } from 'kibana/server';
import { IScopedClusterClient } from 'kibana/server';
import { deserializeDataStream, deserializeDataStreamList } from '../../../../common/lib';
import { DataStreamFromEs } from '../../../../common/types';
import { RouteDependencies } from '../../../types';
@ -68,30 +68,23 @@ const enhanceDataStreams = ({
});
};
const getDataStreams = (client: ElasticsearchClient, name = '*') => {
// TODO update when elasticsearch client has update requestParams for 'indices.getDataStream'
return client.transport.request({
path: `/_data_stream/${encodeURIComponent(name)}`,
method: 'GET',
querystring: {
const getDataStreams = (client: IScopedClusterClient, name = '*') => {
return client.asCurrentUser.indices.getDataStream({
name,
expand_wildcards: 'all',
},
});
};
const getDataStreamsStats = (client: ElasticsearchClient, name = '*') => {
return client.transport.request({
path: `/_data_stream/${encodeURIComponent(name)}/_stats`,
method: 'GET',
querystring: {
const getDataStreamsStats = (client: IScopedClusterClient, name = '*') => {
return client.asCurrentUser.indices.dataStreamsStats({
name,
expand_wildcards: 'all',
human: true,
expand_wildcards: 'all',
},
});
};
const getDataStreamsPrivileges = (client: ElasticsearchClient, names: string[]) => {
return client.security.hasPrivileges({
const getDataStreamsPrivileges = (client: IScopedClusterClient, names: string[]) => {
return client.asCurrentUser.security.hasPrivileges({
body: {
index: [
{
@ -109,15 +102,15 @@ export function registerGetAllRoute({ router, lib: { handleEsError }, config }:
});
router.get(
{ path: addBasePath('/data_streams'), validate: { query: querySchema } },
async (ctx, req, response) => {
const { asCurrentUser } = ctx.core.elasticsearch.client;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const includeStats = (req.query as TypeOf<typeof querySchema>).includeStats === 'true';
const includeStats = (request.query as TypeOf<typeof querySchema>).includeStats === 'true';
try {
let {
const {
body: { data_streams: dataStreams },
} = await getDataStreams(asCurrentUser);
} = await getDataStreams(client);
let dataStreamsStats;
let dataStreamsPrivileges;
@ -125,24 +118,26 @@ export function registerGetAllRoute({ router, lib: { handleEsError }, config }:
if (includeStats) {
({
body: { data_streams: dataStreamsStats },
} = await getDataStreamsStats(asCurrentUser));
} = await getDataStreamsStats(client));
}
if (config.isSecurityEnabled() && dataStreams.length > 0) {
({ body: dataStreamsPrivileges } = await getDataStreamsPrivileges(
asCurrentUser,
dataStreams.map((dataStream: DataStreamFromEs) => dataStream.name)
client,
dataStreams.map((dataStream) => dataStream.name)
));
}
dataStreams = enhanceDataStreams({
const enhancedDataStreams = enhanceDataStreams({
// @ts-expect-error @elastic/elasticsearch DataStreamFromEs incompatible with IndicesGetDataStreamIndicesGetDataStreamItem
dataStreams,
// @ts-expect-error @elastic/elasticsearch StatsFromEs incompatible with IndicesDataStreamsStatsDataStreamsStatsItem
dataStreamsStats,
// @ts-expect-error PrivilegesFromEs incompatible with ApplicationsPrivileges
// @ts-expect-error @elastic/elasticsearch PrivilegesFromEs incompatible with ApplicationsPrivileges
dataStreamsPrivileges,
});
return response.ok({ body: deserializeDataStreamList(dataStreams) });
return response.ok({ body: deserializeDataStreamList(enhancedDataStreams) });
} catch (error) {
return handleEsError({ error, response });
}
@ -159,9 +154,9 @@ export function registerGetOneRoute({ router, lib: { handleEsError }, config }:
path: addBasePath('/data_streams/{name}'),
validate: { params: paramsSchema },
},
async (ctx, req, response) => {
const { name } = req.params as TypeOf<typeof paramsSchema>;
const { asCurrentUser } = ctx.core.elasticsearch.client;
async (context, request, response) => {
const { name } = request.params as TypeOf<typeof paramsSchema>;
const { client } = context.core.elasticsearch;
try {
const [
{
@ -170,23 +165,22 @@ export function registerGetOneRoute({ router, lib: { handleEsError }, config }:
{
body: { data_streams: dataStreamsStats },
},
] = await Promise.all([
getDataStreams(asCurrentUser, name),
getDataStreamsStats(asCurrentUser, name),
]);
] = await Promise.all([getDataStreams(client, name), getDataStreamsStats(client, name)]);
if (dataStreams[0]) {
let dataStreamsPrivileges;
if (config.isSecurityEnabled()) {
({ body: dataStreamsPrivileges } = await getDataStreamsPrivileges(asCurrentUser, [
({ body: dataStreamsPrivileges } = await getDataStreamsPrivileges(client, [
dataStreams[0].name,
]));
}
const enhancedDataStreams = enhanceDataStreams({
// @ts-expect-error @elastic/elasticsearch DataStreamFromEs incompatible with IndicesGetDataStreamIndicesGetDataStreamItem
dataStreams,
// @ts-expect-error @elastic/elasticsearch StatsFromEs incompatible with IndicesDataStreamsStatsDataStreamsStatsItem
dataStreamsStats,
// @ts-expect-error PrivilegesFromEs incompatible with ApplicationsPrivileges
// @ts-expect-error @elastic/elasticsearch PrivilegesFromEs incompatible with ApplicationsPrivileges
dataStreamsPrivileges,
});
const body = deserializeDataStream(enhancedDataStreams[0]);

View file

@ -14,31 +14,24 @@ const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerClearCacheRoute({ router, lib }: RouteDependencies) {
export function registerClearCacheRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/clear_cache'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const payload = req.body as typeof bodySchema.type;
const { indices = [] } = payload;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indices = [] } = request.body as typeof bodySchema.type;
const params = {
expandWildcards: 'none',
expand_wildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.clearCache', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
await client.asCurrentUser.indices.clearCache(params);
return response.ok();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -14,31 +14,24 @@ const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerCloseRoute({ router, lib }: RouteDependencies) {
export function registerCloseRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/close'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const payload = req.body as typeof bodySchema.type;
const { indices = [] } = payload;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indices = [] } = request.body as typeof bodySchema.type;
const params = {
expandWildcards: 'none',
expand_wildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.close', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
await client.asCurrentUser.indices.close(params);
return response.ok();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -14,31 +14,24 @@ const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerDeleteRoute({ router, lib }: RouteDependencies) {
export function registerDeleteRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/delete'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indices = [] } = request.body as typeof bodySchema.type;
const params = {
expandWildcards: 'none',
expand_wildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.delete', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
await client.asCurrentUser.indices.delete(params);
return response.ok();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -14,31 +14,24 @@ const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerFlushRoute({ router, lib }: RouteDependencies) {
export function registerFlushRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/flush'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indices = [] } = request.body as typeof bodySchema.type;
const params = {
expandWildcards: 'none',
expand_wildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.flush', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
await client.asCurrentUser.indices.flush(params);
return response.ok();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -15,7 +15,7 @@ const bodySchema = schema.object({
maxNumSegments: schema.maybe(schema.number()),
});
export function registerForcemergeRoute({ router, lib }: RouteDependencies) {
export function registerForcemergeRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{
path: addBasePath('/indices/forcemerge'),
@ -23,10 +23,11 @@ export function registerForcemergeRoute({ router, lib }: RouteDependencies) {
body: bodySchema,
},
},
async (ctx, req, res) => {
const { maxNumSegments, indices = [] } = req.body as typeof bodySchema.type;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { maxNumSegments, indices = [] } = request.body as typeof bodySchema.type;
const params = {
expandWildcards: 'none',
expand_wildcards: 'none',
index: indices,
};
@ -35,17 +36,10 @@ export function registerForcemergeRoute({ router, lib }: RouteDependencies) {
}
try {
await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.forcemerge', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
await client.asCurrentUser.indices.forcemerge(params);
return response.ok();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -14,33 +14,20 @@ const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerFreezeRoute({ router, lib }: RouteDependencies) {
export function registerFreezeRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/freeze'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
const params = {
path: `/${encodeURIComponent(indices.join(','))}/_freeze`,
method: 'POST',
};
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indices = [] } = request.body as typeof bodySchema.type;
try {
await await ctx.core.elasticsearch.legacy.client.callAsCurrentUser(
'transport.request',
params
);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
await client.asCurrentUser.indices.freeze({
index: indices.join(','),
});
}
// Case: default
throw e;
return response.ok();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -9,23 +9,21 @@ import { fetchIndices } from '../../../lib/fetch_indices';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
export function registerListRoute({ router, indexDataEnricher, lib }: RouteDependencies) {
router.get({ path: addBasePath('/indices'), validate: false }, async (ctx, req, res) => {
export function registerListRoute({
router,
indexDataEnricher,
lib: { handleEsError },
}: RouteDependencies) {
router.get(
{ path: addBasePath('/indices'), validate: false },
async (context, request, response) => {
const { client } = context.core.elasticsearch;
try {
const indices = await fetchIndices(
ctx.core.elasticsearch.legacy.client.callAsCurrentUser,
indexDataEnricher
const indices = await fetchIndices(client, indexDataEnricher);
return response.ok({ body: indices });
} catch (error) {
return handleEsError({ error, response });
}
}
);
return res.ok({ body: indices });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
}
});
}

View file

@ -14,31 +14,24 @@ const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerOpenRoute({ router, lib }: RouteDependencies) {
export function registerOpenRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/open'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indices = [] } = request.body as typeof bodySchema.type;
const params = {
expandWildcards: 'none',
expand_wildcards: 'none',
format: 'json',
index: indices,
};
try {
await await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.open', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
await client.asCurrentUser.indices.open(params);
return response.ok();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -14,31 +14,24 @@ const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerRefreshRoute({ router, lib }: RouteDependencies) {
export function registerRefreshRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/refresh'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const body = req.body as typeof bodySchema.type;
const { indices = [] } = body;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indices = [] } = request.body as typeof bodySchema.type;
const params = {
expandWildcards: 'none',
expand_wildcards: 'none',
format: 'json',
index: indices,
};
try {
await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.refresh', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
await client.asCurrentUser.indices.refresh(params);
return response.ok();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -17,28 +17,22 @@ const bodySchema = schema.maybe(
})
);
export function registerReloadRoute({ router, indexDataEnricher, lib }: RouteDependencies) {
export function registerReloadRoute({
router,
indexDataEnricher,
lib: { handleEsError },
}: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/reload'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const { indexNames = [] } = (req.body as typeof bodySchema.type) ?? {};
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indexNames = [] } = (request.body as typeof bodySchema.type) ?? {};
try {
const indices = await fetchIndices(
ctx.core.elasticsearch.legacy.client.callAsCurrentUser,
indexDataEnricher,
indexNames
);
return res.ok({ body: indices });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
const indices = await fetchIndices(client, indexDataEnricher, indexNames);
return response.ok({ body: indices });
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -14,28 +14,20 @@ const bodySchema = schema.object({
indices: schema.arrayOf(schema.string()),
});
export function registerUnfreezeRoute({ router, lib }: RouteDependencies) {
export function registerUnfreezeRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{ path: addBasePath('/indices/unfreeze'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const { indices = [] } = req.body as typeof bodySchema.type;
const params = {
path: `/${encodeURIComponent(indices.join(','))}/_unfreeze`,
method: 'POST',
};
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indices = [] } = request.body as typeof bodySchema.type;
try {
await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('transport.request', params);
return res.ok();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
await client.asCurrentUser.indices.unfreeze({
index: indices.join(','),
});
}
// Case: default
throw e;
return response.ok();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -21,32 +21,23 @@ function formatHit(hit: { [key: string]: { mappings: any } }, indexName: string)
};
}
export function registerMappingRoute({ router, lib }: RouteDependencies) {
export function registerMappingRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.get(
{ path: addBasePath('/mapping/{indexName}'), validate: { params: paramsSchema } },
async (ctx, req, res) => {
const { indexName } = req.params as typeof paramsSchema.type;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indexName } = request.params as typeof paramsSchema.type;
const params = {
expand_wildcards: 'none',
index: indexName,
};
try {
const hit = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser(
'indices.getMapping',
params
);
const response = formatHit(hit, indexName);
return res.ok({ body: response });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
const { body: hit } = await client.asCurrentUser.indices.getMapping(params);
const responseBody = formatHit(hit, indexName);
return response.ok({ body: responseBody });
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -21,34 +21,25 @@ function formatHit(hit: { [key: string]: {} }) {
return hit[key];
}
export function registerLoadRoute({ router, lib }: RouteDependencies) {
export function registerLoadRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.get(
{ path: addBasePath('/settings/{indexName}'), validate: { params: paramsSchema } },
async (ctx, req, res) => {
const { indexName } = req.params as typeof paramsSchema.type;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indexName } = request.params as typeof paramsSchema.type;
const params = {
expandWildcards: 'none',
flatSettings: false,
expand_wildcards: 'none',
flat_settings: false,
local: false,
includeDefaults: true,
include_defaults: true,
index: indexName,
};
try {
const hit = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser(
'indices.getSettings',
params
);
return res.ok({ body: formatHit(hit) });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
const { body: hit } = await client.asCurrentUser.indices.getSettings(params);
return response.ok({ body: formatHit(hit) });
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -16,37 +16,28 @@ const paramsSchema = schema.object({
indexName: schema.string(),
});
export function registerUpdateRoute({ router, lib }: RouteDependencies) {
export function registerUpdateRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.put(
{
path: addBasePath('/settings/{indexName}'),
validate: { body: bodySchema, params: paramsSchema },
},
async (ctx, req, res) => {
const { indexName } = req.params as typeof paramsSchema.type;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indexName } = request.params as typeof paramsSchema.type;
const params = {
ignoreUnavailable: true,
allowNoIndices: false,
expandWildcards: 'none',
ignore_unavailable: true,
allow_no_indices: false,
expand_wildcards: 'none',
index: indexName,
body: req.body,
body: request.body,
};
try {
const response = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser(
'indices.putSettings',
params
);
return res.ok({ body: response });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
const { body: responseBody } = await client.asCurrentUser.indices.putSettings(params);
return response.ok({ body: responseBody });
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import type { estypes } from '@elastic/elasticsearch';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
@ -14,40 +15,37 @@ const paramsSchema = schema.object({
indexName: schema.string(),
});
function formatHit(hit: { _shards: any; indices: { [key: string]: any } }, indexName: string) {
interface Hit {
_shards: unknown;
indices?: Record<string, estypes.IndicesStatsIndicesStats>;
}
function formatHit(hit: Hit, indexName: string) {
const { _shards, indices } = hit;
const stats = indices[indexName];
const stats = indices![indexName];
return {
_shards,
stats,
};
}
export function registerStatsRoute({ router, lib }: RouteDependencies) {
export function registerStatsRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.get(
{ path: addBasePath('/stats/{indexName}'), validate: { params: paramsSchema } },
async (ctx, req, res) => {
const { indexName } = req.params as typeof paramsSchema.type;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { indexName } = request.params as typeof paramsSchema.type;
const params = {
expand_wildcards: 'none',
index: indexName,
};
try {
const hit = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser(
'indices.stats',
params
);
return res.ok({ body: formatHit(hit, indexName) });
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
const { body: hit } = await client.asCurrentUser.indices.stats(params);
return response.ok({ body: formatHit(hit, indexName) });
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -5,32 +5,33 @@
* 2.0.
*/
import { IScopedClusterClient } from 'kibana/server';
import { serializeTemplate, serializeLegacyTemplate } from '../../../../common/lib';
import { TemplateDeserialized, LegacyTemplateSerialized } from '../../../../common';
import { CallAsCurrentUser } from '../../../types';
export const doesTemplateExist = async ({
name,
callAsCurrentUser,
client,
isLegacy,
}: {
name: string;
callAsCurrentUser: CallAsCurrentUser;
client: IScopedClusterClient;
isLegacy?: boolean;
}) => {
if (isLegacy) {
return await callAsCurrentUser('indices.existsTemplate', { name });
return await client.asCurrentUser.indices.existsTemplate({ name });
}
return await callAsCurrentUser('dataManagement.existsTemplate', { name });
return await client.asCurrentUser.indices.existsIndexTemplate({ name });
};
export const saveTemplate = async ({
template,
callAsCurrentUser,
client,
isLegacy,
}: {
template: TemplateDeserialized;
callAsCurrentUser: CallAsCurrentUser;
client: IScopedClusterClient;
isLegacy?: boolean;
}) => {
const serializedTemplate = isLegacy
@ -48,8 +49,9 @@ export const saveTemplate = async ({
aliases,
} = serializedTemplate as LegacyTemplateSerialized;
return await callAsCurrentUser('indices.putTemplate', {
return await client.asCurrentUser.indices.putTemplate({
name: template.name,
// @ts-expect-error @elastic/elasticsearch not assignable to parameter of type 'IndicesPutTemplateRequest'
order,
body: {
index_patterns,
@ -61,8 +63,9 @@ export const saveTemplate = async ({
});
}
return await callAsCurrentUser('dataManagement.saveComposableIndexTemplate', {
return await client.asCurrentUser.indices.putIndexTemplate({
name: template.name,
// @ts-expect-error @elastic/elasticsearch Type 'LegacyTemplateSerialized | TemplateSerialized' is not assignable
body: serializedTemplate,
});
};

View file

@ -15,25 +15,27 @@ import { saveTemplate, doesTemplateExist } from './lib';
const bodySchema = templateSchema;
export function registerCreateRoute({ router, lib }: RouteDependencies) {
export function registerCreateRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{ path: addBasePath('/index_templates'), validate: { body: bodySchema } },
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const template = req.body as TemplateDeserialized;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const template = request.body as TemplateDeserialized;
try {
const {
_kbnMeta: { isLegacy },
} = template;
// Check that template with the same name doesn't already exist
const templateExists = await doesTemplateExist({
const { body: templateExists } = await doesTemplateExist({
name: template.name,
callAsCurrentUser,
client,
isLegacy,
});
if (templateExists) {
return res.conflict({
return response.conflict({
body: new Error(
i18n.translate('xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage', {
defaultMessage: "There is already a template with name '{name}'.",
@ -45,24 +47,12 @@ export function registerCreateRoute({ router, lib }: RouteDependencies) {
});
}
try {
// Otherwise create new index template
const response = await saveTemplate({ template, callAsCurrentUser, isLegacy });
const { body: responseBody } = await saveTemplate({ template, client, isLegacy });
return res.ok({ body: response });
} catch (e) {
if (lib.isEsError(e)) {
const error = lib.parseEsError(e.response);
return res.customError({
statusCode: e.statusCode,
body: {
message: error.message,
attributes: error,
},
});
}
// Case: default
throw e;
return response.ok({ body: responseBody });
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -9,7 +9,6 @@ import { schema, TypeOf } from '@kbn/config-schema';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
import { wrapEsError } from '../../helpers';
import { TemplateDeserialized } from '../../../../common';
@ -22,16 +21,19 @@ const bodySchema = schema.object({
),
});
export function registerDeleteRoute({ router }: RouteDependencies) {
export function registerDeleteRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{
path: addBasePath('/delete_index_templates'),
validate: { body: bodySchema },
},
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const { templates } = req.body as TypeOf<typeof bodySchema>;
const response: { templatesDeleted: Array<TemplateDeserialized['name']>; errors: any[] } = {
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { templates } = request.body as TypeOf<typeof bodySchema>;
const responseBody: {
templatesDeleted: Array<TemplateDeserialized['name']>;
errors: any[];
} = {
templatesDeleted: [],
errors: [],
};
@ -40,26 +42,26 @@ export function registerDeleteRoute({ router }: RouteDependencies) {
templates.map(async ({ name, isLegacy }) => {
try {
if (isLegacy) {
await callAsCurrentUser('indices.deleteTemplate', {
await client.asCurrentUser.indices.deleteTemplate({
name,
});
} else {
await callAsCurrentUser('dataManagement.deleteComposableIndexTemplate', {
await client.asCurrentUser.indices.deleteIndexTemplate({
name,
});
}
return response.templatesDeleted.push(name);
} catch (e) {
return response.errors.push({
return responseBody.templatesDeleted.push(name);
} catch (error) {
return responseBody.errors.push({
name,
error: wrapEsError(e),
error: handleEsError({ error, response }),
});
}
})
);
return res.ok({ body: response });
return response.ok({ body: responseBody });
}
);
}

View file

@ -17,17 +17,19 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) {
router.get({ path: addBasePath('/index_templates'), validate: false }, async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.get(
{ path: addBasePath('/index_templates'), validate: false },
async (context, request, response) => {
const { client } = context.core.elasticsearch;
try {
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser);
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client);
const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate');
const { index_templates: templatesEs } = await callAsCurrentUser(
'dataManagement.getComposableIndexTemplates'
);
const { body: legacyTemplatesEs } = await client.asCurrentUser.indices.getTemplate();
const {
body: { index_templates: templatesEs },
} = await client.asCurrentUser.indices.getIndexTemplate();
const legacyTemplates = deserializeLegacyTemplateList(
legacyTemplatesEs,
@ -40,18 +42,12 @@ export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDepende
legacyTemplates,
};
return res.ok({ body });
return response.ok({ body });
} catch (error) {
if (isEsError(error)) {
return res.customError({
statusCode: error.statusCode,
body: error,
});
return handleEsError({ error, response });
}
// Case: default
throw error;
}
});
);
}
const paramsSchema = schema.object({
@ -63,26 +59,27 @@ const querySchema = schema.object({
legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])),
});
export function registerGetOneRoute({ router, lib }: RouteDependencies) {
export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.get(
{
path: addBasePath('/index_templates/{name}'),
validate: { params: paramsSchema, query: querySchema },
},
async (ctx, req, res) => {
const { name } = req.params as TypeOf<typeof paramsSchema>;
const { callAsCurrentUser } = ctx.dataManagement!.client;
const isLegacy = (req.query as TypeOf<typeof querySchema>).legacy === 'true';
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { name } = request.params as TypeOf<typeof paramsSchema>;
const isLegacy = (request.query as TypeOf<typeof querySchema>).legacy === 'true';
try {
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser);
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client);
if (isLegacy) {
const indexTemplateByName = await callAsCurrentUser('indices.getTemplate', { name });
const { body: indexTemplateByName } = await client.asCurrentUser.indices.getTemplate({
name,
});
if (indexTemplateByName[name]) {
return res.ok({
return response.ok({
body: deserializeLegacyTemplate(
{ ...indexTemplateByName[name], name },
cloudManagedTemplatePrefix
@ -91,11 +88,11 @@ export function registerGetOneRoute({ router, lib }: RouteDependencies) {
}
} else {
const {
index_templates: indexTemplates,
} = await callAsCurrentUser('dataManagement.getComposableIndexTemplate', { name });
body: { index_templates: indexTemplates },
} = await client.asCurrentUser.indices.getIndexTemplate({ name });
if (indexTemplates.length > 0) {
return res.ok({
return response.ok({
body: deserializeTemplate(
{ ...indexTemplates[0].index_template, name },
cloudManagedTemplatePrefix
@ -104,16 +101,9 @@ export function registerGetOneRoute({ router, lib }: RouteDependencies) {
}
}
return res.notFound();
} catch (e) {
if (lib.isEsError(e)) {
return res.customError({
statusCode: e.statusCode,
body: e,
});
}
// Case: default
throw e;
return response.notFound();
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -12,35 +12,30 @@ import { addBasePath } from '../index';
const bodySchema = schema.object({}, { unknowns: 'allow' });
export function registerSimulateRoute({ router, lib }: RouteDependencies) {
export function registerSimulateRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.post(
{
path: addBasePath('/index_templates/simulate'),
validate: { body: bodySchema },
},
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const template = req.body as TypeOf<typeof bodySchema>;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const template = request.body as TypeOf<typeof bodySchema>;
try {
const templatePreview = await callAsCurrentUser('dataManagement.simulateTemplate', {
body: template,
});
return res.ok({ body: templatePreview });
} catch (e) {
if (lib.isEsError(e)) {
const error = lib.parseEsError(e.response);
return res.customError({
statusCode: e.statusCode,
const { body: templatePreview } = await client.asCurrentUser.indices.simulateTemplate({
body: {
message: error.message,
attributes: error,
...template,
// Until ES fixes a bug on their side we need to send a fake index pattern
// that won't match any indices.
// Issue: https://github.com/elastic/elasticsearch/issues/59152
index_patterns: ['a_fake_index_pattern_that_wont_match_any_indices'],
},
});
}
// Case: default
throw e;
return response.ok({ body: templatePreview });
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -18,45 +18,35 @@ const paramsSchema = schema.object({
name: schema.string(),
});
export function registerUpdateRoute({ router, lib }: RouteDependencies) {
export function registerUpdateRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.put(
{
path: addBasePath('/index_templates/{name}'),
validate: { body: bodySchema, params: paramsSchema },
},
async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const { name } = req.params as typeof paramsSchema.type;
const template = req.body as TemplateDeserialized;
async (context, request, response) => {
const { client } = context.core.elasticsearch;
const { name } = request.params as typeof paramsSchema.type;
const template = request.body as TemplateDeserialized;
try {
const {
_kbnMeta: { isLegacy },
} = template;
// Verify the template exists (ES will throw 404 if not)
const doesExist = await doesTemplateExist({ name, callAsCurrentUser, isLegacy });
const { body: templateExists } = await doesTemplateExist({ name, client, isLegacy });
if (!doesExist) {
return res.notFound();
if (!templateExists) {
return response.notFound();
}
try {
// Next, update index template
const response = await saveTemplate({ template, callAsCurrentUser, isLegacy });
const { body: responseBody } = await saveTemplate({ template, client, isLegacy });
return res.ok({ body: response });
} catch (e) {
if (lib.isEsError(e)) {
const error = lib.parseEsError(e.response);
return res.customError({
statusCode: e.statusCode,
body: {
message: error.message,
attributes: error,
},
});
}
// Case: default
throw e;
return response.ok({ body: responseBody });
} catch (error) {
return handleEsError({ error, response });
}
}
);

View file

@ -1,58 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
const extractCausedByChain = (causedBy: any = {}, accumulator: any[] = []): any => {
const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/naming-convention
if (reason) {
accumulator.push(reason);
}
if (caused_by) {
return extractCausedByChain(caused_by, accumulator);
}
return accumulator;
};
/**
* Wraps an error thrown by the ES JS client into a Boom error response and returns it
*
* @param err Object Error thrown by ES JS client
* @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages
* @return Object Boom error response
*/
export const wrapEsError = (err: any, statusCodeToMessageMap: any = {}) => {
const { statusCode, response } = err;
const {
error: {
root_cause = [], // eslint-disable-line @typescript-eslint/naming-convention
caused_by = {}, // eslint-disable-line @typescript-eslint/naming-convention
} = {},
} = JSON.parse(response);
// If no custom message if specified for the error's status code, just
// wrap the error as a Boom error response, include the additional information from ES, and return it
if (!statusCodeToMessageMap[statusCode]) {
// const boomError = Boom.boomify(err, { statusCode });
const error: any = { statusCode };
// The caused_by chain has the most information so use that if it's available. If not then
// settle for the root_cause.
const causedByChain = extractCausedByChain(caused_by);
const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined;
error.cause = causedByChain.length ? causedByChain : defaultCause;
return error;
}
// Otherwise, use the custom message to create a Boom error response and
// return it
const message = statusCodeToMessageMap[statusCode];
return { message, statusCode };
};

View file

@ -5,10 +5,10 @@
* 2.0.
*/
import { CallAsCurrentUser } from '../types';
import { IScopedClusterClient } from 'kibana/server';
import { Index } from '../index';
export type Enricher = (indices: Index[], callAsCurrentUser: CallAsCurrentUser) => Promise<Index[]>;
export type Enricher = (indices: Index[], client: IScopedClusterClient) => Promise<Index[]>;
export class IndexDataEnricher {
private readonly _enrichers: Enricher[] = [];
@ -19,14 +19,14 @@ export class IndexDataEnricher {
public enrichIndices = async (
indices: Index[],
callAsCurrentUser: CallAsCurrentUser
client: IScopedClusterClient
): Promise<Index[]> => {
let enrichedIndices = indices;
for (let i = 0; i < this.enrichers.length; i++) {
const dataEnricher = this.enrichers[i];
try {
const dataEnricherResponse = await dataEnricher(enrichedIndices, callAsCurrentUser);
const dataEnricherResponse = await dataEnricher(enrichedIndices, client);
enrichedIndices = dataEnricherResponse;
} catch (e) {
// silently swallow enricher response errors

View file

@ -5,8 +5,4 @@
* 2.0.
*/
export {
isEsError,
parseEsError,
handleEsError,
} from '../../../../src/plugins/es_ui_shared/server';
export { handleEsError } from '../../../../src/plugins/es_ui_shared/server';

View file

@ -5,17 +5,13 @@
* 2.0.
*/
import type {
LegacyScopedClusterClient,
ILegacyScopedClusterClient,
IRouter,
RequestHandlerContext,
} from 'src/core/server';
import { IRouter } from 'src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { SecurityPluginSetup } from '../../security/server';
import { IndexDataEnricher } from './services';
import { isEsError, parseEsError, handleEsError } from './shared_imports';
import { handleEsError } from './shared_imports';
export interface Dependencies {
security: SecurityPluginSetup;
@ -24,39 +20,12 @@ export interface Dependencies {
}
export interface RouteDependencies {
router: IndexManagementRouter;
router: IRouter;
config: {
isSecurityEnabled: () => boolean;
};
indexDataEnricher: IndexDataEnricher;
lib: {
isEsError: typeof isEsError;
parseEsError: typeof parseEsError;
handleEsError: typeof handleEsError;
};
}
export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser'];
export interface DataManagementContext {
client: ILegacyScopedClusterClient;
}
/**
* @internal
*/
export interface IndexManagementApiRequestHandlerContext {
client: ILegacyScopedClusterClient;
}
/**
* @internal
*/
export interface IndexManagementRequestHandlerContext extends RequestHandlerContext {
dataManagement: IndexManagementApiRequestHandlerContext;
}
/**
* @internal
*/
export type IndexManagementRouter = IRouter<IndexManagementRequestHandlerContext>;

View file

@ -5,20 +5,19 @@
* 2.0.
*/
import { IScopedClusterClient } from 'kibana/server';
import { Index } from '../../../plugins/index_management/server';
export const rollupDataEnricher = async (indicesList: Index[], callWithRequest: any) => {
export const rollupDataEnricher = async (indicesList: Index[], client: IScopedClusterClient) => {
if (!indicesList || !indicesList.length) {
return Promise.resolve(indicesList);
}
const params = {
path: '/_all/_rollup/data',
method: 'GET',
};
try {
const rollupJobData = await callWithRequest('transport.request', params);
const { body: rollupJobData } = await client.asCurrentUser.rollup.getRollupIndexCaps({
index: '_all',
});
return indicesList.map((index) => {
const isRollupIndex = !!rollupJobData[index.name];
return {

View file

@ -281,8 +281,19 @@ export default function ({ getService }: FtrProviderContext) {
expect(body).to.eql({
statusCode: 404,
error: 'Not Found',
message:
'[resource_not_found_exception] component template matching [component_does_not_exist] not found',
message: 'component template matching [component_does_not_exist] not found',
attributes: {
error: {
reason: 'component template matching [component_does_not_exist] not found',
root_cause: [
{
reason: 'component template matching [component_does_not_exist] not found',
type: 'resource_not_found_exception',
},
],
type: 'resource_not_found_exception',
},
},
});
});
});
@ -356,10 +367,19 @@ export default function ({ getService }: FtrProviderContext) {
const uri = `${API_BASE_PATH}/component_templates/${componentTemplateName},${COMPONENT_DOES_NOT_EXIST}`;
const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200);
expect(body.itemsDeleted).to.eql([componentTemplateName]);
expect(body.errors[0].name).to.eql(COMPONENT_DOES_NOT_EXIST);
expect(body.errors[0].error.msg).to.contain('resource_not_found_exception');
expect(body.errors[0].error.payload.attributes.error).to.eql({
root_cause: [
{
type: 'resource_not_found_exception',
reason: 'component_does_not_exist',
},
],
type: 'resource_not_found_exception',
reason: 'component_does_not_exist',
});
});
});

View file

@ -14,11 +14,11 @@ import { DataStream } from '../../../../../plugins/index_management/common';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const es = getService('legacyEs');
const es = getService('es');
const createDataStream = async (name: string) => {
// A data stream requires an index template before it can be created.
await es.dataManagement.saveComposableIndexTemplate({
await es.indices.putIndexTemplate({
name,
body: {
// We need to match the names of backing indices with this template.
@ -36,15 +36,15 @@ export default function ({ getService }: FtrProviderContext) {
},
});
await es.dataManagement.createDataStream({ name });
await es.indices.createDataStream({ name });
};
const deleteComposableIndexTemplate = async (name: string) => {
await es.dataManagement.deleteComposableIndexTemplate({ name });
await es.indices.deleteIndexTemplate({ name });
};
const deleteDataStream = async (name: string) => {
await es.dataManagement.deleteDataStream({ name });
await es.indices.deleteDataStream({ name });
await deleteComposableIndexTemplate(name);
};

View file

@ -56,13 +56,17 @@ export default function ({ getService }) {
const index = await createIndex();
// Make sure the index is open
const [cat1] = await catIndex(index);
const {
body: [cat1],
} = await catIndex(index);
expect(cat1.status).to.be('open');
await closeIndex(index).expect(200);
// Make sure the index has been closed
const [cat2] = await catIndex(index);
const {
body: [cat2],
} = await catIndex(index);
expect(cat2.status).to.be('close');
});
});
@ -78,13 +82,17 @@ export default function ({ getService }) {
await closeIndex(index);
// Make sure the index is closed
const [cat1] = await catIndex(index);
const {
body: [cat1],
} = await catIndex(index);
expect(cat1.status).to.be('close');
await openIndex(index).expect(200);
// Make sure the index is opened
const [cat2] = await catIndex(index);
const {
body: [cat2],
} = await catIndex(index);
expect(cat2.status).to.be('open');
});
});
@ -93,12 +101,12 @@ export default function ({ getService }) {
it('should delete an index', async () => {
const index = await createIndex();
const indices1 = await catIndex(undefined, 'i');
const { body: indices1 } = await catIndex(undefined, 'i');
expect(indices1.map((index) => index.i)).to.contain(index);
await deleteIndex([index]).expect(200);
const indices2 = await catIndex(undefined, 'i');
const { body: indices2 } = await catIndex(undefined, 'i');
expect(indices2.map((index) => index.i)).not.to.contain(index);
});
@ -112,12 +120,16 @@ export default function ({ getService }) {
it('should flush an index', async () => {
const index = await createIndex();
const { indices: indices1 } = await indexStats(index, 'flush');
const {
body: { indices: indices1 },
} = await indexStats(index, 'flush');
expect(indices1[index].total.flush.total).to.be(0);
await flushIndex(index).expect(200);
const { indices: indices2 } = await indexStats(index, 'flush');
const {
body: { indices: indices2 },
} = await indexStats(index, 'flush');
expect(indices2[index].total.flush.total).to.be(1);
});
});
@ -126,12 +138,16 @@ export default function ({ getService }) {
it('should refresh an index', async () => {
const index = await createIndex();
const { indices: indices1 } = await indexStats(index, 'refresh');
const {
body: { indices: indices1 },
} = await indexStats(index, 'refresh');
const previousRefreshes = indices1[index].total.refresh.total;
await refreshIndex(index).expect(200);
const { indices: indices2 } = await indexStats(index, 'refresh');
const {
body: { indices: indices2 },
} = await indexStats(index, 'refresh');
expect(indices2[index].total.refresh.total).to.be(previousRefreshes + 1);
});
});
@ -153,12 +169,16 @@ export default function ({ getService }) {
const index = await createIndex();
// "sth" correspond to search throttling. Frozen indices are normal indices
// with search throttling turned on.
const [cat1] = await catIndex(index, 'sth');
const {
body: [cat1],
} = await catIndex(index, 'sth');
expect(cat1.sth).to.be('false');
await freeze(index).expect(200);
const [cat2] = await catIndex(index, 'sth');
const {
body: [cat2],
} = await catIndex(index, 'sth');
expect(cat2.sth).to.be('true');
});
});
@ -168,11 +188,15 @@ export default function ({ getService }) {
const index = await createIndex();
await freeze(index).expect(200);
const [cat1] = await catIndex(index, 'sth');
const {
body: [cat1],
} = await catIndex(index, 'sth');
expect(cat1.sth).to.be('true');
await unfreeze(index).expect(200);
const [cat2] = await catIndex(index, 'sth');
const {
body: [cat2],
} = await catIndex(index, 'sth');
expect(cat2.sth).to.be('false');
});
});

View file

@ -13,7 +13,7 @@ import { getRandomString } from './random';
* @param {ElasticsearchClient} es The Elasticsearch client instance
*/
export const initElasticsearchHelpers = (getService) => {
const es = getService('legacyEs');
const es = getService('es');
const esDeleteAllIndices = getService('esDeleteAllIndices');
let indicesCreated = [];
@ -42,11 +42,11 @@ export const initElasticsearchHelpers = (getService) => {
componentTemplatesCreated.push(componentTemplate.name);
}
return es.dataManagement.saveComponentTemplate(componentTemplate);
return es.cluster.putComponentTemplate(componentTemplate);
};
const deleteComponentTemplate = (componentTemplateName) => {
return es.dataManagement.deleteComponentTemplate({ name: componentTemplateName });
return es.cluster.deleteComponentTemplate({ name: componentTemplateName });
};
const cleanUpComponentTemplates = () =>

View file

@ -193,8 +193,8 @@ export default function ({ getService }) {
});
it('should parse the ES error and return the cause', async () => {
const templateName = `template-${getRandomString()}`;
const payload = getTemplatePayload(templateName, [getRandomString()]);
const templateName = `template-create-parse-es-error}`;
const payload = getTemplatePayload(templateName, ['create-parse-es-error']);
const runtime = {
myRuntimeField: {
type: 'boolean',
@ -207,9 +207,9 @@ export default function ({ getService }) {
const { body } = await createTemplate(payload).expect(400);
expect(body.attributes).an('object');
expect(body.attributes.message).contain('template after composition is invalid');
expect(body.attributes.error.reason).contain('template after composition is invalid');
// one of the item of the cause array should point to our script
expect(body.attributes.cause.join(',')).contain('"hello with error');
expect(body.attributes.causes.join(',')).contain('"hello with error');
});
});
@ -220,7 +220,7 @@ export default function ({ getService }) {
await createTemplate(indexTemplate).expect(200);
let catTemplateResponse = await catTemplate(templateName);
let { body: catTemplateResponse } = await catTemplate(templateName);
const { name, version } = indexTemplate;
@ -234,7 +234,7 @@ export default function ({ getService }) {
200
);
catTemplateResponse = await catTemplate(templateName);
({ body: catTemplateResponse } = await catTemplate(templateName));
expect(
catTemplateResponse.find(({ name: templateName }) => templateName === name).version
@ -247,7 +247,7 @@ export default function ({ getService }) {
await createTemplate(legacyIndexTemplate).expect(200);
let catTemplateResponse = await catTemplate(templateName);
let { body: catTemplateResponse } = await catTemplate(templateName);
const { name, version } = legacyIndexTemplate;
@ -262,7 +262,7 @@ export default function ({ getService }) {
templateName
).expect(200);
catTemplateResponse = await catTemplate(templateName);
({ body: catTemplateResponse } = await catTemplate(templateName));
expect(
catTemplateResponse.find(({ name: templateName }) => templateName === name).version
@ -270,8 +270,8 @@ export default function ({ getService }) {
});
it('should parse the ES error and return the cause', async () => {
const templateName = `template-${getRandomString()}`;
const payload = getTemplatePayload(templateName, [getRandomString()]);
const templateName = `template-update-parse-es-error}`;
const payload = getTemplatePayload(templateName, ['update-parse-es-error']);
const runtime = {
myRuntimeField: {
type: 'keyword',
@ -292,7 +292,7 @@ export default function ({ getService }) {
expect(body.attributes).an('object');
// one of the item of the cause array should point to our script
expect(body.attributes.cause.join(',')).contain('"hello with error');
expect(body.attributes.causes.join(',')).contain('"hello with error');
});
});
@ -306,7 +306,7 @@ export default function ({ getService }) {
throw new Error(`Error creating template: ${createStatus} ${createBody.message}`);
}
let catTemplateResponse = await catTemplate(templateName);
let { body: catTemplateResponse } = await catTemplate(templateName);
expect(
catTemplateResponse.find((template) => template.name === payload.name).name
@ -322,7 +322,7 @@ export default function ({ getService }) {
expect(deleteBody.errors).to.be.empty;
expect(deleteBody.templatesDeleted[0]).to.equal(templateName);
catTemplateResponse = await catTemplate(templateName);
({ body: catTemplateResponse } = await catTemplate(templateName));
expect(catTemplateResponse.find((template) => template.name === payload.name)).to.equal(
undefined
@ -335,7 +335,7 @@ export default function ({ getService }) {
await createTemplate(payload).expect(200);
let catTemplateResponse = await catTemplate(templateName);
let { body: catTemplateResponse } = await catTemplate(templateName);
expect(
catTemplateResponse.find((template) => template.name === payload.name).name
@ -348,7 +348,7 @@ export default function ({ getService }) {
expect(body.errors).to.be.empty;
expect(body.templatesDeleted[0]).to.equal(templateName);
catTemplateResponse = await catTemplate(templateName);
({ body: catTemplateResponse } = await catTemplate(templateName));
expect(catTemplateResponse.find((template) => template.name === payload.name)).to.equal(
undefined

View file

@ -9,7 +9,6 @@ import { format as formatUrl } from 'url';
import * as legacyElasticsearch from 'elasticsearch';
import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config';
@ -20,6 +19,5 @@ export function LegacyEsProvider({ getService }) {
apiVersion: DEFAULT_API_VERSION,
host: formatUrl(config.get('servers.elasticsearch')),
requestTimeout: config.get('timeouts.esRequestTimeout'),
plugins: [indexManagementEsClientPlugin],
});
}