From b25d852204168bf0997df51d5cfed474e96c1584 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Wed, 16 Sep 2020 11:29:32 -0600 Subject: [PATCH] [index_pattern_management]: Replace calls to `/elasticsearch/_msearch` with internal route. (#77564) --- .../components/scripting_help/test_script.tsx | 5 +- .../components/field_editor/field_editor.tsx | 1 - .../field_editor/lib/validate_script.ts | 58 ++---- .../public/components/field_editor/types.ts | 3 +- .../index_pattern_management/server/plugin.ts | 41 +--- .../server/routes/index.ts | 21 ++ .../routes/preview_scripted_field.test.ts | 181 ++++++++++++++++++ .../server/routes/preview_scripted_field.ts | 74 +++++++ .../server/routes/resolve_index.ts | 60 ++++++ 9 files changed, 361 insertions(+), 83 deletions(-) create mode 100644 src/plugins/index_pattern_management/server/routes/index.ts create mode 100644 src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts create mode 100644 src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts create mode 100644 src/plugins/index_pattern_management/server/routes/resolve_index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx index 77c6698fdc337..d5f04810daf56 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx @@ -81,7 +81,7 @@ export class TestScript extends Component { } previewScript = async (searchContext?: { query?: Query | undefined }) => { - const { indexPattern, lang, name, script, executeScript } = this.props; + const { indexPattern, name, script, executeScript } = this.props; if (!script || script.length === 0) { return; @@ -104,7 +104,6 @@ export class TestScript extends Component { const scriptResponse = await executeScript({ name: name as string, - lang, script, indexPatternTitle: indexPattern.title, query, @@ -122,7 +121,7 @@ export class TestScript extends Component { this.setState({ isLoading: false, - previewData: scriptResponse.hits.hits.map((hit: any) => ({ + previewData: scriptResponse.hits?.hits.map((hit: any) => ({ _id: hit._id, ...hit._source, ...hit.fields, diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 4857a402cc4b2..2b484d1d837bf 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -784,7 +784,6 @@ export class FieldEditor extends PureComponent => { - // Using _msearch because _search with index name in path dorks everything up - const header = { - index: indexPatternTitle, - ignore_unavailable: true, - }; - - const search = { - query: { - match_all: {}, - } as Query['query'], - script_fields: { - [name]: { - script: { - lang, - source: script, - }, - }, - }, - _source: undefined as string[] | undefined, - size: 10, - timeout: '30s', - }; - - if (additionalFields.length > 0) { - search._source = additionalFields; - } - - if (query) { - search.query = query; - } - - const body = `${JSON.stringify(header)}\n${JSON.stringify(search)}\n`; - const esResp = await http.fetch('/elasticsearch/_msearch', { method: 'POST', body }); - // unwrap _msearch response - return esResp.responses[0]; + return http + .post('/internal/index-pattern-management/preview_scripted_field', { + body: JSON.stringify({ + index: indexPatternTitle, + name, + script, + query, + additionalFields, + }), + }) + .then((res) => ({ + status: res.statusCode, + hits: res.body.hits, + })) + .catch((err) => ({ + status: err.statusCode, + error: err.body.attributes.error, + })); }; export const isScriptValid = async ({ name, - lang, script, indexPatternTitle, http, }: { name: string; - lang: string; script: string; indexPatternTitle: string; http: HttpStart; }) => { const scriptResponse = await executeScript({ name, - lang, script, indexPatternTitle, http, diff --git a/src/plugins/index_pattern_management/public/components/field_editor/types.ts b/src/plugins/index_pattern_management/public/components/field_editor/types.ts index 7519cc05e7fae..d716b9d557282 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/types.ts +++ b/src/plugins/index_pattern_management/public/components/field_editor/types.ts @@ -28,7 +28,6 @@ export interface Sample { export interface ExecuteScriptParams { name: string; - lang: string; script: string; indexPatternTitle: string; query?: Query['query']; @@ -38,7 +37,7 @@ export interface ExecuteScriptParams { export interface ExecuteScriptResult { status: number; - hits: { hits: any[] }; + hits?: { hits: any[] }; error?: any; } diff --git a/src/plugins/index_pattern_management/server/plugin.ts b/src/plugins/index_pattern_management/server/plugin.ts index ecca45cbcc453..2bed6761ef362 100644 --- a/src/plugins/index_pattern_management/server/plugin.ts +++ b/src/plugins/index_pattern_management/server/plugin.ts @@ -18,7 +18,8 @@ */ import { PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; -import { schema } from '@kbn/config-schema'; + +import { registerPreviewScriptedFieldRoute, registerResolveIndexRoute } from './routes'; export class IndexPatternManagementPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} @@ -26,42 +27,8 @@ export class IndexPatternManagementPlugin implements Plugin { public setup(core: CoreSetup) { const router = core.http.createRouter(); - router.get( - { - path: '/internal/index-pattern-management/resolve_index/{query}', - validate: { - params: schema.object({ - query: schema.string(), - }), - query: schema.object({ - expand_wildcards: schema.maybe( - schema.oneOf([ - schema.literal('all'), - schema.literal('open'), - schema.literal('closed'), - schema.literal('hidden'), - schema.literal('none'), - ]) - ), - }), - }, - }, - async (context, req, res) => { - const queryString = req.query.expand_wildcards - ? { expand_wildcards: req.query.expand_wildcards } - : null; - const result = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'transport.request', - { - method: 'GET', - path: `/_resolve/index/${encodeURIComponent(req.params.query)}${ - queryString ? '?' + new URLSearchParams(queryString).toString() : '' - }`, - } - ); - return res.ok({ body: result }); - } - ); + registerPreviewScriptedFieldRoute(router); + registerResolveIndexRoute(router); } public start() {} diff --git a/src/plugins/index_pattern_management/server/routes/index.ts b/src/plugins/index_pattern_management/server/routes/index.ts new file mode 100644 index 0000000000000..14d53f10970d5 --- /dev/null +++ b/src/plugins/index_pattern_management/server/routes/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './preview_scripted_field'; +export * from './resolve_index'; diff --git a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts new file mode 100644 index 0000000000000..5de6ddf351c02 --- /dev/null +++ b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts @@ -0,0 +1,181 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, RequestHandlerContext } from 'src/core/server'; +import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { registerPreviewScriptedFieldRoute } from './preview_scripted_field'; + +describe('preview_scripted_field route', () => { + let mockCoreSetup: MockedKeys; + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + }); + + it('handler calls /_search with the given request', async () => { + const response = { body: { responses: [{ hits: { _id: 'hi' } }] } }; + const mockClient = { search: jest.fn().mockResolvedValue(response) }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockClient } }, + }, + }; + const mockBody = { + index: 'kibana_sample_data_logs', + name: 'my_scripted_field', + script: `doc['foo'].value`, + }; + const mockQuery = {}; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerPreviewScriptedFieldRoute(mockCoreSetup.http.createRouter()); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.search.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "_source": undefined, + "body": Object { + "query": Object { + "match_all": Object {}, + }, + "script_fields": Object { + "my_scripted_field": Object { + "script": Object { + "lang": "painless", + "source": "doc['foo'].value", + }, + }, + }, + }, + "index": "kibana_sample_data_logs", + "size": 10, + "timeout": "30s", + } + `); + + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: response }); + }); + + it('uses optional parameters when they are provided', async () => { + const response = { body: { responses: [{ hits: { _id: 'hi' } }] } }; + const mockClient = { search: jest.fn().mockResolvedValue(response) }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockClient } }, + }, + }; + const mockBody = { + index: 'kibana_sample_data_logs', + name: 'my_scripted_field', + script: `doc['foo'].value`, + query: { + bool: { some: 'query' }, + }, + additionalFields: ['a', 'b', 'c'], + }; + const mockQuery = {}; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerPreviewScriptedFieldRoute(mockCoreSetup.http.createRouter()); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.search.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "_source": Array [ + "a", + "b", + "c", + ], + "body": Object { + "query": Object { + "bool": Object { + "some": "query", + }, + }, + "script_fields": Object { + "my_scripted_field": Object { + "script": Object { + "lang": "painless", + "source": "doc['foo'].value", + }, + }, + }, + }, + "index": "kibana_sample_data_logs", + "size": 10, + "timeout": "30s", + } + `); + }); + + it('handler throws an error if the search throws an error', async () => { + const response = { + statusCode: 400, + message: 'oops', + }; + const mockClient = { search: jest.fn().mockReturnValue(Promise.reject(response)) }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockClient } }, + }, + }; + const mockBody = { searches: [{ header: {}, body: {} }] }; + const mockQuery = {}; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerPreviewScriptedFieldRoute(mockCoreSetup.http.createRouter()); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.search).toBeCalled(); + expect(mockResponse.customError).toBeCalled(); + + const error: any = mockResponse.customError.mock.calls[0][0]; + expect(error.statusCode).toBe(400); + expect(error.body).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "error": "oops", + }, + "message": "oops", + } + `); + }); +}); diff --git a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts new file mode 100644 index 0000000000000..849263748aeaa --- /dev/null +++ b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export function registerPreviewScriptedFieldRoute(router: IRouter): void { + router.post( + { + path: '/internal/index-pattern-management/preview_scripted_field', + validate: { + body: schema.object({ + index: schema.string(), + name: schema.string(), + script: schema.string(), + query: schema.maybe(schema.object({}, { unknowns: 'allow' })), + additionalFields: schema.maybe(schema.arrayOf(schema.string())), + }), + }, + }, + async (context, request, res) => { + const client = context.core.elasticsearch.client.asCurrentUser; + const { index, name, script, query, additionalFields } = request.body; + + try { + const response = await client.search({ + index, + _source: additionalFields && additionalFields.length > 0 ? additionalFields : undefined, + size: 10, + timeout: '30s', + body: { + query: query ?? { match_all: {} }, + script_fields: { + [name]: { + script: { + lang: 'painless', + source: script, + }, + }, + }, + }, + }); + + return res.ok({ body: response }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); +} diff --git a/src/plugins/index_pattern_management/server/routes/resolve_index.ts b/src/plugins/index_pattern_management/server/routes/resolve_index.ts new file mode 100644 index 0000000000000..1d3d89a94e391 --- /dev/null +++ b/src/plugins/index_pattern_management/server/routes/resolve_index.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export function registerResolveIndexRoute(router: IRouter): void { + router.get( + { + path: '/internal/index-pattern-management/resolve_index/{query}', + validate: { + params: schema.object({ + query: schema.string(), + }), + query: schema.object({ + expand_wildcards: schema.maybe( + schema.oneOf([ + schema.literal('all'), + schema.literal('open'), + schema.literal('closed'), + schema.literal('hidden'), + schema.literal('none'), + ]) + ), + }), + }, + }, + async (context, req, res) => { + const queryString = req.query.expand_wildcards + ? { expand_wildcards: req.query.expand_wildcards } + : null; + const result = await context.core.elasticsearch.legacy.client.callAsCurrentUser( + 'transport.request', + { + method: 'GET', + path: `/_resolve/index/${encodeURIComponent(req.params.query)}${ + queryString ? '?' + new URLSearchParams(queryString).toString() : '' + }`, + } + ); + return res.ok({ body: result }); + } + ); +}