From fc516bacbd367baea0b06447c3710f693ebab06d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 5 Feb 2021 14:13:51 -0700 Subject: [PATCH] [index patterns] Add pattern validation method to index patterns fetcher (#90170) --- ...lugins-data-server.indexpatternsfetcher.md | 1 + ...tternsfetcher.validatepatternlistactive.md | 24 ++++ .../fetcher/index_patterns_fetcher.test.ts | 72 ++++++++++++ .../fetcher/index_patterns_fetcher.ts | 40 ++++++- src/plugins/data/server/server.api.md | 1 + .../fields_for_wildcard_route/response.js | 110 ++++++++++-------- 6 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md create mode 100644 src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md index 3ba3c862bf16a..608d738676bcf 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md @@ -22,4 +22,5 @@ export declare class IndexPatternsFetcher | --- | --- | --- | | [getFieldsForTimePattern(options)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsfortimepattern.md) | | Get a list of field objects for a time pattern | | [getFieldsForWildcard(options)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md) | | Get a list of field objects for an index pattern that may contain wildcards | +| [validatePatternListActive(patternList)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md) | | Returns an index pattern list of only those index pattern strings in the given list that return indices | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md new file mode 100644 index 0000000000000..8944c41204323 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) > [validatePatternListActive](./kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md) + +## IndexPatternsFetcher.validatePatternListActive() method + +Returns an index pattern list of only those index pattern strings in the given list that return indices + +Signature: + +```typescript +validatePatternListActive(patternList: string[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| patternList | string[] | | + +Returns: + +`Promise` + diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts new file mode 100644 index 0000000000000..ffdd47e5cdf49 --- /dev/null +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts @@ -0,0 +1,72 @@ +/* + * 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 { IndexPatternsFetcher } from '.'; +import { ElasticsearchClient } from 'kibana/server'; +import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json'; + +describe('Index Pattern Fetcher - server', () => { + let indexPatterns: IndexPatternsFetcher; + let esClient: ElasticsearchClient; + const emptyResponse = { + body: { + count: 0, + }, + }; + const response = { + body: { + count: 1115, + }, + }; + const patternList = ['a', 'b', 'c']; + beforeEach(() => { + esClient = ({ + count: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + }); + + it('Removes pattern without matching indices', async () => { + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual(['b', 'c']); + }); + + it('Returns all patterns when all match indices', async () => { + esClient = ({ + count: jest.fn().mockResolvedValue(response), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual(patternList); + }); + it('Removes pattern when "index_not_found_exception" error is thrown', async () => { + class ServerError extends Error { + public body?: Record; + constructor( + message: string, + public readonly statusCode: number, + errBody?: Record + ) { + super(message); + this.body = errBody; + } + } + + esClient = ({ + count: jest + .fn() + .mockResolvedValueOnce(response) + .mockRejectedValue( + new ServerError('index_not_found_exception', 404, indexNotFoundException) + ), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual([patternList[0]]); + }); +}); diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index cc8bfe28bbc9a..3acdde33f599e 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -58,9 +58,16 @@ export class IndexPatternsFetcher { rollupIndex?: string; }): Promise { const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options; + const patternList = Array.isArray(pattern) ? pattern : pattern.split(','); + let patternListActive: string[] = patternList; + // if only one pattern, don't bother with validation. We let getFieldCapabilities fail if the single pattern is bad regardless + if (patternList.length > 1) { + patternListActive = await this.validatePatternListActive(patternList); + } const fieldCapsResponse = await getFieldCapabilities( this.elasticsearchClient, - pattern, + // if none of the patterns are active, pass the original list to get an error + patternListActive.length > 0 ? patternListActive : patternList, metaFields, { allow_no_indices: fieldCapsOptions @@ -68,6 +75,7 @@ export class IndexPatternsFetcher { : this.allowNoIndices, } ); + if (type === 'rollup' && rollupIndex) { const rollupFields: FieldDescriptor[] = []; const rollupIndexCapabilities = getCapabilitiesForRollupIndices( @@ -118,4 +126,34 @@ export class IndexPatternsFetcher { } return await getFieldCapabilities(this.elasticsearchClient, indices, metaFields); } + + /** + * Returns an index pattern list of only those index pattern strings in the given list that return indices + * + * @param patternList string[] + * @return {Promise} + */ + async validatePatternListActive(patternList: string[]) { + const result = await Promise.all( + patternList + .map((pattern) => + this.elasticsearchClient.count({ + index: pattern, + }) + ) + .map((p) => + p.catch((e) => { + if (e.body.error.type === 'index_not_found_exception') { + return { body: { count: 0 } }; + } + throw e; + }) + ) + ); + return result.reduce( + (acc: string[], { body: { count } }, patternListIndex) => + count > 0 ? [...acc, patternList[patternListIndex]] : acc, + [] + ); + } } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 68582a9d877e9..3b1440f211bfe 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -885,6 +885,7 @@ export class IndexPatternsFetcher { type?: string; rollupIndex?: string; }): Promise; + validatePatternListActive(patternList: string[]): Promise; } // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js index e84052e58dac4..87c5aa535ccd9 100644 --- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js @@ -17,6 +17,55 @@ export default function ({ getService }) { expect(resp.body.fields).to.eql(sortBy(resp.body.fields, 'name')); }; + const testFields = [ + { + type: 'boolean', + esTypes: ['boolean'], + searchable: true, + aggregatable: true, + name: 'bar', + readFromDocValues: true, + }, + { + type: 'string', + esTypes: ['text'], + searchable: true, + aggregatable: false, + name: 'baz', + readFromDocValues: false, + }, + { + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + name: 'baz.keyword', + readFromDocValues: true, + subType: { multi: { parent: 'baz' } }, + }, + { + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: true, + name: 'foo', + readFromDocValues: true, + }, + { + aggregatable: true, + esTypes: ['keyword'], + name: 'nestedField.child', + readFromDocValues: true, + searchable: true, + subType: { + nested: { + path: 'nestedField', + }, + }, + type: 'string', + }, + ]; + describe('fields_for_wildcard_route response', () => { before(() => esArchiver.load('index_patterns/basic_index')); after(() => esArchiver.unload('index_patterns/basic_index')); @@ -26,54 +75,7 @@ export default function ({ getService }) { .get('/api/index_patterns/_fields_for_wildcard') .query({ pattern: 'basic_index' }) .expect(200, { - fields: [ - { - type: 'boolean', - esTypes: ['boolean'], - searchable: true, - aggregatable: true, - name: 'bar', - readFromDocValues: true, - }, - { - type: 'string', - esTypes: ['text'], - searchable: true, - aggregatable: false, - name: 'baz', - readFromDocValues: false, - }, - { - type: 'string', - esTypes: ['keyword'], - searchable: true, - aggregatable: true, - name: 'baz.keyword', - readFromDocValues: true, - subType: { multi: { parent: 'baz' } }, - }, - { - type: 'number', - esTypes: ['long'], - searchable: true, - aggregatable: true, - name: 'foo', - readFromDocValues: true, - }, - { - aggregatable: true, - esTypes: ['keyword'], - name: 'nestedField.child', - readFromDocValues: true, - searchable: true, - subType: { - nested: { - path: 'nestedField', - }, - }, - type: 'string', - }, - ], + fields: testFields, }) .then(ensureFieldsAreSorted); }); @@ -162,11 +164,19 @@ export default function ({ getService }) { .then(ensureFieldsAreSorted); }); - it('returns 404 when the pattern does not exist', async () => { + it('returns fields when one pattern exists and the other does not', async () => { + await supertest + .get('/api/index_patterns/_fields_for_wildcard') + .query({ pattern: 'bad_index,basic_index' }) + .expect(200, { + fields: testFields, + }); + }); + it('returns 404 when no patterns exist', async () => { await supertest .get('/api/index_patterns/_fields_for_wildcard') .query({ - pattern: '[non-existing-pattern]its-invalid-*', + pattern: 'bad_index', }) .expect(404); });