From c97eff3baef30f9918d9ae6c046f40f247c94ff4 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 12 Mar 2019 15:14:51 -0500 Subject: [PATCH] Add button for adding `index.query.default_field` setting to metricbeat indices (#32829) * Add button for adding `index.query.default_field` setting to metricbeat indices * Add button to 'group by index' view * Refactor to more generic API * Remove comment * Update functional tests --- .../tabs/checkup/deprecations/cell.tsx | 25 +- .../deprecations/default_fields/button.tsx | 129 + .../tabs/checkup/deprecations/index_table.tsx | 19 +- .../tabs/checkup/deprecations/list.test.tsx | 2 + .../tabs/checkup/deprecations/list.tsx | 10 +- .../plugins/upgrade_assistant/server/index.ts | 2 + .../server/lib/query_default_field.test.ts | 106 + .../server/lib/query_default_field.ts | 80 + .../server/routes/query_default_field.test.ts | 87 + .../server/routes/query_default_field.ts | 67 + .../upgrade_assistant/metricbeat/data.json.gz | Bin 0 -> 19570 bytes .../metricbeat/mappings.json | 10683 ++++++++++++++++ .../upgrade_assistant/index.js | 1 + .../upgrade_assistant/query_default_field.js | 46 + 14 files changed, 11240 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/default_fields/button.tsx create mode 100644 x-pack/plugins/upgrade_assistant/server/lib/query_default_field.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/lib/query_default_field.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/query_default_field.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/query_default_field.ts create mode 100644 x-pack/test/functional/es_archives/upgrade_assistant/metricbeat/data.json.gz create mode 100644 x-pack/test/functional/es_archives/upgrade_assistant/metricbeat/mappings.json create mode 100644 x-pack/test/upgrade_assistant_integration/upgrade_assistant/query_default_field.js diff --git a/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/cell.tsx b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/cell.tsx index de4f947f40a49..1962f96836f0e 100644 --- a/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/cell.tsx @@ -16,13 +16,16 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { FixDefaultFieldsButton } from './default_fields/button'; import { DeleteTasksButton } from './delete_tasks_button'; import { ReindexButton } from './reindex'; interface DeprecationCellProps { items?: Array<{ title?: string; body: string }>; - reindexIndexName?: string; - deleteIndexName?: string; + indexName?: string; + reindex?: boolean; + deleteIndex?: boolean; + needsDefaultFields?: boolean; docUrl?: string; headline?: string; healthColor?: string; @@ -35,8 +38,10 @@ interface DeprecationCellProps { export const DeprecationCell: StatelessComponent = ({ headline, healthColor, - reindexIndexName, - deleteIndexName, + indexName, + reindex, + deleteIndex, + needsDefaultFields, docUrl, items = [], children, @@ -78,17 +83,23 @@ export const DeprecationCell: StatelessComponent = ({ ))} - {reindexIndexName && ( + {reindex && ( - + )} - {deleteIndexName && ( + {deleteIndex && ( )} + + {needsDefaultFields && ( + + + + )} diff --git a/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/default_fields/button.tsx b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/default_fields/button.tsx new file mode 100644 index 0000000000000..cbf20cb3927e7 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/default_fields/button.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; + +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { kfetch } from 'ui/kfetch'; +import { LoadingState } from '../../../../types'; + +/** + * Field types used by Metricbeat to generate the default_field setting. + * Matches Beats code here: + * https://github.com/elastic/beats/blob/eee127cb59b56f2ed7c7e317398c3f79c4158216/libbeat/template/processor.go#L104 + */ +const METRICBEAT_DEFAULT_FIELD_TYPES: ReadonlySet = new Set(['keyword', 'text', 'ip']); +const METRICBEAT_OTHER_DEFAULT_FIELDS: ReadonlySet = new Set(['fields.*']); + +interface FixDefaultFieldsButtonProps { + indexName: string; +} + +interface FixDefaultFieldsButtonState { + fixLoadingState?: LoadingState; +} + +/** + * Renders a button if given index is a valid Metricbeat index to add a default_field setting. + */ +export class FixDefaultFieldsButton extends React.Component< + FixDefaultFieldsButtonProps, + FixDefaultFieldsButtonState +> { + constructor(props: FixDefaultFieldsButtonProps) { + super(props); + this.state = {}; + } + + public render() { + const { fixLoadingState } = this.state; + + if (!this.isMetricbeatIndex()) { + return null; + } + + const buttonProps: any = { size: 's', onClick: this.fixMetricbeatIndex }; + let buttonContent: ReactNode; + + switch (fixLoadingState) { + case LoadingState.Loading: + buttonProps.disabled = true; + buttonProps.isLoading = true; + buttonContent = ( + + ); + break; + case LoadingState.Success: + buttonProps.iconSide = 'left'; + buttonProps.iconType = 'check'; + buttonProps.disabled = true; + buttonContent = ( + + ); + break; + case LoadingState.Error: + buttonProps.color = 'danger'; + buttonProps.iconSide = 'left'; + buttonProps.iconType = 'cross'; + buttonContent = ( + + ); + break; + default: + buttonContent = ( + + ); + } + + return {buttonContent}; + } + + private isMetricbeatIndex = () => { + return this.props.indexName.startsWith('metricbeat-'); + }; + + private fixMetricbeatIndex = async () => { + if (!this.isMetricbeatIndex()) { + return; + } + + this.setState({ + fixLoadingState: LoadingState.Loading, + }); + + try { + await kfetch({ + pathname: `/api/upgrade_assistant/add_query_default_field/${this.props.indexName}`, + method: 'POST', + body: JSON.stringify({ + fieldTypes: [...METRICBEAT_DEFAULT_FIELD_TYPES], + otherFields: [...METRICBEAT_OTHER_DEFAULT_FIELDS], + }), + }); + + this.setState({ + fixLoadingState: LoadingState.Success, + }); + } catch (e) { + this.setState({ + fixLoadingState: LoadingState.Error, + }); + } + }; +} diff --git a/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.tsx b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.tsx index eeba8f74962a4..9e420fb22a9d0 100644 --- a/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/index_table.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiBasicTable } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; +import { FixDefaultFieldsButton } from './default_fields/button'; import { DeleteTasksButton } from './delete_tasks_button'; import { ReindexButton } from './reindex'; @@ -18,6 +19,7 @@ export interface IndexDeprecationDetails { index: string; reindex: boolean; delete: boolean; + needsDefaultFields: boolean; details?: string; } @@ -137,7 +139,10 @@ export class IndexDeprecationTableUI extends React.Component< // NOTE: this naive implementation assumes all indices in the table are // should show the reindex button. This should work for known usecases. const { indices } = this.props; - if (!indices.find(i => i.reindex || i.delete)) { + const showDeleteButton = indices.find(i => i.delete === true); + const showReindexButton = indices.find(i => i.reindex === true); + const showNeedsDefaultFieldsButton = indices.find(i => i.needsDefaultFields === true); + if (!showDeleteButton && !showReindexButton && !showNeedsDefaultFieldsButton) { return null; } @@ -145,11 +150,13 @@ export class IndexDeprecationTableUI extends React.Component< actions: [ { render(indexDep: IndexDeprecationDetails) { - return indexDep.delete ? ( - - ) : ( - - ); + if (showDeleteButton) { + return ; + } else if (showReindexButton) { + return ; + } else { + return ; + } }, }, ], diff --git a/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.test.tsx b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.test.tsx index 3fd299cc5f979..00119834bc39a 100644 --- a/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.test.tsx @@ -74,12 +74,14 @@ describe('DeprecationList', () => { "delete": false, "details": undefined, "index": "0", + "needsDefaultFields": false, "reindex": false, }, Object { "delete": false, "details": undefined, "index": "1", + "needsDefaultFields": false, "reindex": false, }, ] diff --git a/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.tsx b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.tsx index 72e4a61472a16..91434fc50f1fd 100644 --- a/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.tsx +++ b/x-pack/plugins/upgrade_assistant/public/components/tabs/checkup/deprecations/list.tsx @@ -17,6 +17,7 @@ import { IndexDeprecationDetails, IndexDeprecationTable } from './index_table'; const OLD_INDEX_MESSAGE = `Index created before ${CURRENT_MAJOR_VERSION}.0`; const DELETE_INDEX_MESSAGE = `.tasks index must be re-created`; +const NEEDS_DEFAULT_FIELD_MESSAGE = 'Number of fields exceeds automatic field expansion limit'; const sortByLevelDesc = (a: DeprecationInfo, b: DeprecationInfo) => { return -1 * (LEVEL_MAP[a.level] - LEVEL_MAP[b.level]); @@ -38,10 +39,10 @@ const MessageDeprecation: StatelessComponent<{ deprecation: EnrichedDeprecationI @@ -97,6 +98,7 @@ export const DeprecationList: StatelessComponent<{ details: dep.details, reindex: dep.message === OLD_INDEX_MESSAGE, delete: dep.message === DELETE_INDEX_MESSAGE, + needsDefaultFields: dep.message === NEEDS_DEFAULT_FIELD_MESSAGE, })); return ; diff --git a/x-pack/plugins/upgrade_assistant/server/index.ts b/x-pack/plugins/upgrade_assistant/server/index.ts index c5a9f5597c7dc..f2ced04d78029 100644 --- a/x-pack/plugins/upgrade_assistant/server/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/index.ts @@ -11,6 +11,7 @@ import { makeUpgradeAssistantUsageCollector } from './lib/telemetry'; import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; import { registerDeleteTasksRoutes } from './routes/delete_tasks'; import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; +import { registerQueryDefaultFieldRoutes } from './routes/query_default_field'; import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/reindex_indices'; import { registerTelemetryRoutes } from './routes/telemetry'; @@ -18,6 +19,7 @@ export function initServer(server: Legacy.Server) { registerClusterCheckupRoutes(server); registerDeleteTasksRoutes(server); registerDeprecationLoggingRoutes(server); + registerQueryDefaultFieldRoutes(server); // The ReindexWorker uses a map of request headers that contain the authentication credentials // for a given reindex. We cannot currently store these in an the .kibana index b/c we do not diff --git a/x-pack/plugins/upgrade_assistant/server/lib/query_default_field.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/query_default_field.test.ts new file mode 100644 index 0000000000000..6d9f7196fcf39 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/query_default_field.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { MappingProperties } from './reindexing/types'; + +import { addDefaultField, generateDefaultFields } from './query_default_field'; + +const defaultFieldTypes = new Set(['keyword', 'text', 'ip']); + +describe('getDefaultFieldList', () => { + it('returns dot-delimited flat list', () => { + const mapping: MappingProperties = { + nested1: { + properties: { + included2: { type: 'ip' }, + ignored2: { type: 'geopoint' }, + nested2: { + properties: { + included3: { type: 'keyword' }, + 'included4.keyword': { type: 'keyword' }, + }, + }, + }, + }, + ignored1: { type: 'object' }, + included1: { type: 'text' }, + }; + + expect(generateDefaultFields(mapping, defaultFieldTypes)).toMatchInlineSnapshot(` +Array [ + "nested1.included2", + "nested1.nested2.included3", + "nested1.nested2.included4.keyword", + "included1", +] +`); + }); +}); + +describe('fixMetricbeatIndex', () => { + const mockMappings = { + 'metricbeat-1': { + mappings: { _doc: { properties: { field1: { type: 'text' }, field2: { type: 'float' } } } }, + }, + }; + const mockSettings = { + 'metricbeat-1': { + settings: {}, + }, + }; + + it('fails if index already has index.query.default_field setting', async () => { + const callWithRequest = jest.fn().mockResolvedValueOnce({ + 'metricbeat-1': { + settings: { index: { query: { default_field: [] } } }, + }, + }); + await expect( + addDefaultField(callWithRequest, {} as any, 'metricbeat-1', defaultFieldTypes) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Index metricbeat-1 already has index.query.default_field set"` + ); + }); + + it('updates index settings with default_field generated from mappings and otherFields', async () => { + const callWithRequest = jest + .fn() + .mockResolvedValueOnce(mockSettings) + .mockResolvedValueOnce(mockMappings) + .mockResolvedValueOnce({ acknowledged: true }); + + await expect( + addDefaultField( + callWithRequest, + {} as any, + 'metricbeat-1', + defaultFieldTypes, + new Set(['fields.*', 'myCustomField']) + ) + ).resolves.toEqual({ + acknowledged: true, + }); + expect(callWithRequest.mock.calls[2]).toMatchInlineSnapshot(` +Array [ + Object {}, + "indices.putSettings", + Object { + "body": Object { + "index": Object { + "query": Object { + "default_field": Array [ + "field1", + "fields.*", + "myCustomField", + ], + }, + }, + }, + "index": "metricbeat-1", + }, +] +`); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/query_default_field.ts b/x-pack/plugins/upgrade_assistant/server/lib/query_default_field.ts new file mode 100644 index 0000000000000..b3e7efaebdfc6 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/query_default_field.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { Request } from 'hapi'; +import { get } from 'lodash'; + +import { CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch'; +import { MappingProperties } from './reindexing/types'; + +/** + * Adds the index.query.default_field setting, generated from the index's mapping. + * + * @param callWithRequest + * @param request + * @param indexName + * @param fieldTypes - Elasticsearch field types that should be used to generate the default_field from the index mapping + * @param otherFields - Other fields that should be included in the generated default_field that do not match `fieldTypes` + */ +export const addDefaultField = async ( + callWithRequest: CallClusterWithRequest, + request: Request, + indexName: string, + fieldTypes: ReadonlySet, + otherFields: ReadonlySet = new Set() +) => { + // Verify index.query.default_field is not already set. + const settings = await callWithRequest(request, 'indices.getSettings', { + index: indexName, + }); + if (get(settings, `${indexName}.settings.index.query.default_field`)) { + throw Boom.badRequest(`Index ${indexName} already has index.query.default_field set`); + } + + // Get the mapping and generate the default_field based on `fieldTypes` + const mappingResp = await callWithRequest(request, 'indices.getMapping', { + index: indexName, + include_type_name: true, + }); + const typeName = Object.getOwnPropertyNames(mappingResp[indexName].mappings)[0]; + const mapping = mappingResp[indexName].mappings[typeName].properties as MappingProperties; + const generatedDefaultFields = new Set(generateDefaultFields(mapping, fieldTypes)); + + // Update the setting with the generated default_field + return await callWithRequest(request, 'indices.putSettings', { + index: indexName, + body: { + index: { query: { default_field: [...generatedDefaultFields, ...otherFields] } }, + }, + }); +}; + +/** + * Recursively walks an index mapping and returns a flat array of dot-delimited + * strings represent all fields that are of a type included in `DEFAULT_FIELD_TYPES` + * @param mapping + */ +export const generateDefaultFields = ( + mapping: MappingProperties, + fieldTypes: ReadonlySet +): string[] => + Object.getOwnPropertyNames(mapping).reduce( + (defaultFields, fieldName) => { + const { type, properties } = mapping[fieldName]; + + if (type && fieldTypes.has(type)) { + defaultFields.push(fieldName); + } else if (properties) { + generateDefaultFields(properties, fieldTypes).forEach(subField => + defaultFields.push(`${fieldName}.${subField}`) + ); + } + + return defaultFields; + }, + [] as string[] + ); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/query_default_field.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/query_default_field.test.ts new file mode 100644 index 0000000000000..1ce986d1bfa19 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/query_default_field.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; + +jest.mock('../lib/es_version_precheck'); + +const mockAddDefaultField = jest.fn(); +jest.mock('../lib/query_default_field', () => ({ + addDefaultField: mockAddDefaultField, +})); + +import { registerQueryDefaultFieldRoutes } from './query_default_field'; + +const callWithRequest = jest.fn(); + +const server = new Server(); +server.plugins = { + elasticsearch: { + getCluster: () => ({ callWithRequest } as any), + } as any, +} as any; +server.config = () => ({ get: () => '' } as any); + +registerQueryDefaultFieldRoutes(server); + +describe('add query default field API', () => { + beforeEach(() => { + mockAddDefaultField.mockClear(); + }); + + it('calls addDefaultField with index, field types, and other fields', async () => { + mockAddDefaultField.mockResolvedValueOnce({ acknowledged: true }); + const resp = await server.inject({ + method: 'POST', + url: '/api/upgrade_assistant/add_query_default_field/myIndex', + payload: { + fieldTypes: ['text', 'boolean'], + otherFields: ['myCustomField'], + }, + }); + + expect(mockAddDefaultField).toHaveBeenCalledWith( + callWithRequest, + expect.anything(), + 'myIndex', + new Set(['text', 'boolean']), + new Set(['myCustomField']) + ); + expect(resp.statusCode).toEqual(200); + expect(resp.payload).toMatchInlineSnapshot(`"{\\"acknowledged\\":true}"`); + }); + + it('calls addDefaultField with index, field types if other fields is not specified', async () => { + mockAddDefaultField.mockResolvedValueOnce({ acknowledged: true }); + const resp = await server.inject({ + method: 'POST', + url: '/api/upgrade_assistant/add_query_default_field/myIndex', + payload: { + fieldTypes: ['text', 'boolean'], + }, + }); + + expect(mockAddDefaultField).toHaveBeenCalledWith( + callWithRequest, + expect.anything(), + 'myIndex', + new Set(['text', 'boolean']), + undefined + ); + expect(resp.statusCode).toEqual(200); + expect(resp.payload).toMatchInlineSnapshot(`"{\\"acknowledged\\":true}"`); + }); + + it('fails if fieldTypes is not specified', async () => { + const resp = await server.inject({ + method: 'POST', + url: '/api/upgrade_assistant/add_query_default_field/myIndex', + }); + + expect(mockAddDefaultField).not.toHaveBeenCalled(); + expect(resp.statusCode).toEqual(400); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/query_default_field.ts b/x-pack/plugins/upgrade_assistant/server/routes/query_default_field.ts new file mode 100644 index 0000000000000..265ebc820fc4b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/query_default_field.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import Joi from 'joi'; +import { Legacy } from 'kibana'; +import _ from 'lodash'; + +import { EsVersionPrecheck } from '../lib/es_version_precheck'; +import { addDefaultField } from '../lib/query_default_field'; + +/** + * Adds routes for detecting and fixing 6.x Metricbeat indices that need the + * `index.query.default_field` index setting added. + * + * @param server + */ +export function registerQueryDefaultFieldRoutes(server: Legacy.Server) { + const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); + + server.route({ + path: '/api/upgrade_assistant/add_query_default_field/{indexName}', + method: 'POST', + options: { + pre: [EsVersionPrecheck], + validate: { + params: Joi.object({ + indexName: Joi.string().required(), + }), + payload: Joi.object({ + fieldTypes: Joi.array() + .items(Joi.string()) + .required(), + otherFields: Joi.array().items(Joi.string()), + }), + }, + }, + async handler(request) { + try { + const { indexName } = request.params; + const { fieldTypes, otherFields } = request.payload as { + fieldTypes: string[]; + otherFields?: string[]; + }; + + return await addDefaultField( + callWithRequest, + request, + indexName, + new Set(fieldTypes), + otherFields ? new Set(otherFields) : undefined + ); + } catch (e) { + if (e.status === 403) { + return Boom.forbidden(e.message); + } + + return Boom.boomify(e, { + statusCode: 500, + }); + } + }, + }); +} diff --git a/x-pack/test/functional/es_archives/upgrade_assistant/metricbeat/data.json.gz b/x-pack/test/functional/es_archives/upgrade_assistant/metricbeat/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..f888874235846a3be27e20e982aed9ac83b8584b GIT binary patch literal 19570 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 o6}akBK+6m)Ejf3z { + beforeEach(async () => { + await esArchiver.load('upgrade_assistant/metricbeat'); + }); + + afterEach(async () => { + await esArchiver.unload('upgrade_assistant/metricbeat'); + }); + + it('adds index.query.default_field to metricbeat index', async () => { + const { body } = await supertest + .post(`/api/upgrade_assistant/add_query_default_field/${indexName}`) + .set('kbn-xsrf', 'xxx') + .send({ fieldTypes: ['text', 'keyword', 'ip'], otherFields: ['fields.*'] }) + .expect(200); + expect(body.acknowledged).to.be(true); + + // The index.query.default_field setting should now be set + const settingsResp = await es.indices.getSettings({ index: indexName }); + expect(settingsResp[indexName].settings.index.query.default_field).to.not.be(undefined); + + // Deprecation message should be gone + const { body: uaBody } = await supertest.get('/api/upgrade_assistant/status').expect(200); + const depMessage = uaBody.indices.find( + dep => dep.index === indexName && dep.message === 'Number of fields exceeds automatic field expansion limit' + ); + expect(depMessage).to.be(undefined); + }); + }); +}