diff --git a/src/plugins/data/server/index_patterns/deprecations/index.ts b/src/plugins/data/server/index_patterns/deprecations/index.ts new file mode 100644 index 0000000000000..98fda3e2419ab --- /dev/null +++ b/src/plugins/data/server/index_patterns/deprecations/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { createScriptedFieldsDeprecationsConfig } from './scripted_fields'; diff --git a/src/plugins/data/server/index_patterns/deprecations/scripted_fields.test.ts b/src/plugins/data/server/index_patterns/deprecations/scripted_fields.test.ts new file mode 100644 index 0000000000000..4ed2779dea857 --- /dev/null +++ b/src/plugins/data/server/index_patterns/deprecations/scripted_fields.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { hasScriptedField } from './scripted_fields'; + +describe('hasScriptedField', () => { + test('valid index pattern object with a scripted field', () => { + expect( + hasScriptedField({ + title: 'kibana_sample_data_logs*', + fields: + '[{"count":0,"script":"return 5;","lang":"painless","name":"test","type":"number","scripted":true,"searchable":true,"aggregatable":true,"readFromDocValues":false,"customLabel":""}]', + }) + ).toBe(true); + }); + + test('valid index pattern object without a scripted field', () => { + expect( + hasScriptedField({ + title: 'kibana_sample_data_logs*', + fields: '[]', + }) + ).toBe(false); + }); + + test('invalid index pattern object', () => { + expect( + hasScriptedField({ + title: 'kibana_sample_data_logs*', + fields: '[...]', + }) + ).toBe(false); + }); +}); diff --git a/src/plugins/data/server/index_patterns/deprecations/scripted_fields.ts b/src/plugins/data/server/index_patterns/deprecations/scripted_fields.ts new file mode 100644 index 0000000000000..7c9ce6f9ed335 --- /dev/null +++ b/src/plugins/data/server/index_patterns/deprecations/scripted_fields.ts @@ -0,0 +1,76 @@ +/* + * 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 { + CoreSetup, + DeprecationsDetails, + GetDeprecationsContext, + RegisterDeprecationsConfig, +} from 'kibana/server'; +import { IndexPatternAttributes } from '../../../common'; + +type IndexPatternAttributesWithFields = Pick; + +export const createScriptedFieldsDeprecationsConfig: ( + core: CoreSetup +) => RegisterDeprecationsConfig = (core: CoreSetup) => ({ + getDeprecations: async (context: GetDeprecationsContext): Promise => { + const finder = context.savedObjectsClient.createPointInTimeFinder( + { + type: 'index-pattern', + perPage: 1000, + fields: ['title', 'fields'], + } + ); + + const indexPatternsWithScriptedFields: IndexPatternAttributesWithFields[] = []; + for await (const response of finder.find()) { + indexPatternsWithScriptedFields.push( + ...response.saved_objects.map((so) => so.attributes).filter(hasScriptedField) + ); + } + + if (indexPatternsWithScriptedFields.length > 0) { + const PREVIEW_LIMIT = 3; + const indexPatternTitles = indexPatternsWithScriptedFields.map((ip) => ip.title); + const titlesPreview = indexPatternTitles.slice(0, PREVIEW_LIMIT).join('; '); + const allTitles = indexPatternTitles.join('; '); + + return [ + { + message: `You have ${indexPatternsWithScriptedFields.length} index patterns (${titlesPreview}...) that use scripted fields. Scripted fields are deprecated and will be removed in future. Use runtime fields instead.`, + documentationUrl: + 'https://www.elastic.co/guide/en/elasticsearch/reference/7.x/runtime.html', // TODO: documentation service is not available serverside https://github.com/elastic/kibana/issues/95389 + level: 'warning', // warning because it is not set in stone WHEN we remove scripted fields, hence this deprecation is not a blocker for 8.0 upgrade + correctiveActions: { + manualSteps: [ + 'Navigate to Stack Management > Kibana > Index Patterns.', + `Update ${indexPatternsWithScriptedFields.length} index patterns that have scripted fields to use runtime fields instead. In most cases, to migrate existing scripts, you'll need to change "return ;" to "emit();". Index patterns with at least one scripted field: ${allTitles}`, + ], + }, + }, + ]; + } else { + return []; + } + }, +}); + +export function hasScriptedField(indexPattern: IndexPatternAttributesWithFields) { + if (indexPattern.fields) { + try { + return JSON.parse(indexPattern.fields).some( + (field: { scripted?: boolean }) => field?.scripted + ); + } catch (e) { + return false; + } + } else { + return false; + } +} diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index c7fd1f7914df9..4269f15127daf 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -28,6 +28,7 @@ import { UiSettingsServerToCommon } from './ui_settings_wrapper'; import { IndexPatternsApiServer } from './index_patterns_api_client'; import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; import { registerIndexPatternsUsageCollector } from './register_index_pattern_usage_collection'; +import { createScriptedFieldsDeprecationsConfig } from './deprecations'; export interface IndexPatternsServiceStart { indexPatternsServiceFactory: ( @@ -88,6 +89,7 @@ export class IndexPatternsServiceProvider implements Plugin { + loadTestFile(require.resolve('./scripted_fields')); + }); +} diff --git a/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts b/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts new file mode 100644 index 0000000000000..168c2b005d80d --- /dev/null +++ b/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts @@ -0,0 +1,68 @@ +/* + * 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 expect from '@kbn/expect'; +import type { DeprecationsGetResponse } from 'src/core/server/types'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('scripted field deprecations', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('no scripted fields deprecations', async () => { + const { body } = await supertest.get('/api/deprecations/'); + + const { deprecations } = body as DeprecationsGetResponse; + const dataPluginDeprecations = deprecations.filter(({ domainId }) => domainId === 'data'); + + expect(dataPluginDeprecations.length).to.be(0); + }); + + it('scripted field deprecation', async () => { + const title = `basic_index`; + await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + scripted: true, + script: "doc['field_name'].value", + }, + bar: { + name: 'bar', + type: 'number', + scripted: true, + script: "doc['field_name'].value", + }, + }, + }, + }); + + const { body } = await supertest.get('/api/deprecations/'); + const { deprecations } = body as DeprecationsGetResponse; + const dataPluginDeprecations = deprecations.filter(({ domainId }) => domainId === 'data'); + + expect(dataPluginDeprecations.length).to.be(1); + expect(dataPluginDeprecations[0].message).to.contain(title); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/index.js b/test/api_integration/apis/index_patterns/index.js index 656b4e506fa23..3dbe01206afa3 100644 --- a/test/api_integration/apis/index_patterns/index.js +++ b/test/api_integration/apis/index_patterns/index.js @@ -17,5 +17,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./default_index_pattern')); loadTestFile(require.resolve('./runtime_fields_crud')); loadTestFile(require.resolve('./integration')); + loadTestFile(require.resolve('./deprecations')); }); }