[index_pattern_management]: Replace calls to /elasticsearch/_msearch with internal route. (#77564)

This commit is contained in:
Luke Elmers 2020-09-16 11:29:32 -06:00 committed by GitHub
parent 23a0dcebe2
commit ba53369170
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 361 additions and 83 deletions

View file

@ -81,7 +81,7 @@ export class TestScript extends Component<TestScriptProps, TestScriptState> {
}
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<TestScriptProps, TestScriptState> {
const scriptResponse = await executeScript({
name: name as string,
lang,
script,
indexPatternTitle: indexPattern.title,
query,
@ -122,7 +121,7 @@ export class TestScript extends Component<TestScriptProps, TestScriptState> {
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,

View file

@ -784,7 +784,6 @@ export class FieldEditor extends PureComponent<FieldEdiorProps, FieldEditorState
const isValid = await isScriptValid({
name: field.name,
lang: field.lang as string,
script: field.script as string,
indexPatternTitle: indexPattern.title,
http: this.context.services.http,

View file

@ -17,72 +17,50 @@
* under the License.
*/
import { Query } from 'src/plugins/data/public';
import { HttpStart } from 'src/core/public';
import { ExecuteScriptParams, ExecuteScriptResult } from '../types';
export const executeScript = async ({
name,
lang,
script,
indexPatternTitle,
query,
additionalFields = [],
http,
}: ExecuteScriptParams): Promise<ExecuteScriptResult> => {
// 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,

View file

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

View file

@ -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<void, void> {
constructor(initializerContext: PluginInitializerContext) {}
@ -26,42 +27,8 @@ export class IndexPatternManagementPlugin implements Plugin<void, void> {
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() {}

View file

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

View file

@ -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<CoreSetup>;
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",
}
`);
});
});

View file

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

View file

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