diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts index 3a847ce939981..3cdf2428bf389 100644 --- a/src/plugins/data_views/public/plugin.ts +++ b/src/plugins/data_views/public/plugin.ts @@ -41,8 +41,11 @@ export class DataViewsPublicPlugin { private readonly hasData = new HasData(); private rollupsEnabled: boolean = false; + private readonly callResolveCluster: boolean; - constructor(private readonly initializerContext: PluginInitializerContext) {} + constructor(private readonly initializerContext: PluginInitializerContext) { + this.callResolveCluster = initializerContext.env.packageInfo.buildFlavor === 'traditional'; + } public setup( core: CoreSetup, @@ -83,7 +86,7 @@ export class DataViewsPublicPlugin const config = this.initializerContext.config.get(); return new DataViewsServicePublic({ - hasData: this.hasData.start(core), + hasData: this.hasData.start(core, this.callResolveCluster), uiSettings: new UiSettingsPublicToCommon(uiSettings), savedObjectsClient: new ContentMagementWrapper(contentManagement.client), apiClient: new DataViewsApiClient(http, async () => { diff --git a/src/plugins/data_views/public/services/has_data.test.ts b/src/plugins/data_views/public/services/has_data.test.ts index b2ccf3828af6b..7118aa5cceaf5 100644 --- a/src/plugins/data_views/public/services/has_data.test.ts +++ b/src/plugins/data_views/public/services/has_data.test.ts @@ -11,241 +11,288 @@ import { coreMock } from '@kbn/core/public/mocks'; import { HasData } from './has_data'; describe('when calling hasData service', () => { - it('should return true for hasESData when indices exist', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; - - // Mock getIndices - const spy = jest.spyOn(http, 'get').mockImplementation(() => - Promise.resolve({ - aliases: [], - data_streams: [], - indices: [ - { - aliases: [], - attributes: ['open'], - name: 'sample_data_logs', - }, - ], - }) - ); - - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = hasDataService.hasESData(); - - expect(spy).toHaveBeenCalledTimes(1); - - expect(await response).toBe(true); - }); - - it('should return false for hasESData when no indices exist', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; - - // Mock getIndices - const spy = jest.spyOn(http, 'get').mockImplementation(() => - Promise.resolve({ - aliases: [], - data_streams: [], - indices: [], - }) - ); - - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = hasDataService.hasESData(); - - expect(spy).toHaveBeenCalledTimes(1); - - expect(await response).toBe(false); - }); - - it('should return false for hasESData when only automatically created sources exist', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; - - // Mock getIndices - const spy = jest.spyOn(http, 'get').mockImplementation((path: any) => - Promise.resolve({ - aliases: [], - data_streams: path.includes('*:*') - ? [] // return empty on remote cluster call - : [ - { - name: 'logs-enterprise_search.api-default', - timestamp_field: '@timestamp', - backing_indices: ['.ds-logs-enterprise_search.api-default-2022.03.07-000001'], - }, - ], - indices: [], - }) - ); - - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = hasDataService.hasESData(); - - expect(spy).toHaveBeenCalledTimes(1); - - expect(await response).toBe(false); - }); - - it('should hit search api in case resolve api throws', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; - - const spyGetIndices = jest - .spyOn(http, 'get') - .mockImplementation(() => Promise.reject(new Error('oops'))); - - const spySearch = jest - .spyOn(http, 'post') - .mockImplementation(() => Promise.resolve({ total: 10 })); - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = await hasDataService.hasESData(); + describe('hasDataView', () => { + it('should return true for hasDataView when server returns true', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + hasDataView: true, + hasUserDataView: true, + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, true); + const response = hasDataService.hasDataView(); + + expect(spy).toHaveBeenCalledTimes(1); - expect(response).toBe(true); + expect(await response).toBe(true); + }); + + it('should return false for hasDataView when server returns false', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + hasDataView: false, + hasUserDataView: true, + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, true); + const response = hasDataService.hasDataView(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await response).toBe(false); + }); + + it('should return true for hasDataView when server throws an error', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest + .spyOn(http, 'get') + .mockImplementation(() => Promise.reject(new Error('Oops'))); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, true); + const response = hasDataService.hasDataView(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await response).toBe(true); + }); + + it('should return false for hasUserDataView when server returns false', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + hasDataView: true, + hasUserDataView: false, + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, true); + const response = hasDataService.hasUserDataView(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await response).toBe(false); + }); + + it('should return true for hasUserDataView when server returns true', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + hasDataView: true, + hasUserDataView: true, + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, true); + const response = hasDataService.hasUserDataView(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await response).toBe(true); + }); - expect(spyGetIndices).toHaveBeenCalledTimes(1); - expect(spySearch).toHaveBeenCalledTimes(1); - }); - - it('should return false in case search api throws', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; + it('should return true for hasUserDataView when server throws an error', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; - const spyGetIndices = jest - .spyOn(http, 'get') - .mockImplementation(() => Promise.reject(new Error('oops'))); + // Mock getIndices + const spy = jest + .spyOn(http, 'get') + .mockImplementation(() => Promise.reject(new Error('Oops'))); - const spySearch = jest - .spyOn(http, 'post') - .mockImplementation(() => Promise.reject(new Error('oops'))); - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = await hasDataService.hasESData(); + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, true); + const response = hasDataService.hasUserDataView(); - expect(response).toBe(true); + expect(spy).toHaveBeenCalledTimes(1); - expect(spyGetIndices).toHaveBeenCalledTimes(1); - expect(spySearch).toHaveBeenCalledTimes(1); + expect(await response).toBe(true); + }); }); + describe('hasESData', () => { + describe('resolve/cluster is available', () => { + it('should return true for hasESData when indices exist', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; - it('should return true for hasDataView when server returns true', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; - - // Mock getIndices - const spy = jest.spyOn(http, 'get').mockImplementation(() => - Promise.resolve({ - hasDataView: true, - hasUserDataView: true, - }) - ); - - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = hasDataService.hasDataView(); - - expect(spy).toHaveBeenCalledTimes(1); - - expect(await response).toBe(true); - }); - - it('should return false for hasDataView when server returns false', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; - - // Mock getIndices - const spy = jest.spyOn(http, 'get').mockImplementation(() => - Promise.resolve({ - hasDataView: false, - hasUserDataView: true, - }) - ); - - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = hasDataService.hasDataView(); - - expect(spy).toHaveBeenCalledTimes(1); + // Mock getIndices + const spy = jest + .spyOn(http, 'get') + .mockImplementation(() => Promise.resolve({ hasEsData: true })); - expect(await response).toBe(false); - }); - - it('should return true for hasDataView when server throws an error', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, true); + const response = hasDataService.hasESData(); - // Mock getIndices - const spy = jest.spyOn(http, 'get').mockImplementation(() => Promise.reject(new Error('Oops'))); + expect(spy).toHaveBeenCalledTimes(1); - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = hasDataService.hasDataView(); + expect(await response).toBe(true); + }); - expect(spy).toHaveBeenCalledTimes(1); + it('should return false for hasESData when no indices exist', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; - expect(await response).toBe(true); - }); + // Mock getIndices + const spy = jest + .spyOn(http, 'get') + .mockImplementation(() => Promise.resolve({ hasEsData: false })); - it('should return false for hasUserDataView when server returns false', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, true); + const response = hasDataService.hasESData(); - // Mock getIndices - const spy = jest.spyOn(http, 'get').mockImplementation(() => - Promise.resolve({ - hasDataView: true, - hasUserDataView: false, - }) - ); + expect(spy).toHaveBeenCalledTimes(1); - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = hasDataService.hasUserDataView(); + expect(await response).toBe(false); + }); + }); - expect(spy).toHaveBeenCalledTimes(1); + describe('resolve/cluster not available', () => { + it('should return true for hasESData when indices exist', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; - expect(await response).toBe(false); - }); + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementationOnce(() => + Promise.resolve({ + aliases: [], + data_streams: [], + indices: [ + { + aliases: [], + attributes: ['open'], + name: 'sample_data_logs', + }, + ], + }) + ); - it('should return true for hasUserDataView when server returns true', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, false); + const response = hasDataService.hasESData(); - // Mock getIndices - const spy = jest.spyOn(http, 'get').mockImplementation(() => - Promise.resolve({ - hasDataView: true, - hasUserDataView: true, - }) - ); + expect(spy).toHaveBeenCalledTimes(1); - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = hasDataService.hasUserDataView(); + expect(await response).toBe(true); + }); - expect(spy).toHaveBeenCalledTimes(1); + it('should return false for hasESData when no indices exist', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; - expect(await response).toBe(true); - }); + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation(() => + Promise.resolve({ + aliases: [], + data_streams: [], + indices: [], + }) + ); - it('should return true for hasUserDataView when server throws an error', async () => { - const coreStart = coreMock.createStart(); - const http = coreStart.http; + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, false); + const response = hasDataService.hasESData(); - // Mock getIndices - const spy = jest.spyOn(http, 'get').mockImplementation(() => Promise.reject(new Error('Oops'))); + expect(spy).toHaveBeenCalledTimes(1); - const hasData = new HasData(); - const hasDataService = hasData.start(coreStart); - const response = hasDataService.hasUserDataView(); + expect(await response).toBe(false); + }); - expect(spy).toHaveBeenCalledTimes(1); + it('should return false for hasESData when only automatically created sources exist', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; - expect(await response).toBe(true); + // Mock getIndices + const spy = jest.spyOn(http, 'get').mockImplementation((path: any) => + Promise.resolve({ + aliases: [], + data_streams: path.includes('*:*') + ? [] // return empty on remote cluster call + : [ + { + name: 'logs-enterprise_search.api-default', + timestamp_field: '@timestamp', + backing_indices: ['.ds-logs-enterprise_search.api-default-2022.03.07-000001'], + }, + ], + indices: [], + }) + ); + + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, false); + const response = hasDataService.hasESData(); + + expect(spy).toHaveBeenCalledTimes(1); + + expect(await response).toBe(false); + }); + + it('should hit search api in case resolve api throws', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + const spyGetIndices = jest + .spyOn(http, 'get') + .mockImplementation(() => Promise.reject(new Error('oops'))); + + const spySearch = jest + .spyOn(http, 'post') + .mockImplementation(() => Promise.resolve({ total: 10 })); + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, false); + const response = await hasDataService.hasESData(); + + expect(response).toBe(true); + + expect(spyGetIndices).toHaveBeenCalledTimes(1); + expect(spySearch).toHaveBeenCalledTimes(1); + }); + + it('should return false in case search api throws', async () => { + const coreStart = coreMock.createStart(); + const http = coreStart.http; + + const spyGetIndices = jest + .spyOn(http, 'get') + .mockImplementation(() => Promise.reject(new Error('oops'))); + + const spySearch = jest + .spyOn(http, 'post') + .mockImplementation(() => Promise.reject(new Error('oops'))); + const hasData = new HasData(); + const hasDataService = hasData.start(coreStart, false); + const response = await hasDataService.hasESData(); + + expect(response).toBe(true); + + expect(spyGetIndices).toHaveBeenCalledTimes(1); + expect(spySearch).toHaveBeenCalledTimes(1); + }); + }); }); }); diff --git a/src/plugins/data_views/public/services/has_data.ts b/src/plugins/data_views/public/services/has_data.ts index 1e32825ec498b..a33e330bdb0ac 100644 --- a/src/plugins/data_views/public/services/has_data.ts +++ b/src/plugins/data_views/public/services/has_data.ts @@ -24,19 +24,43 @@ export class HasData { return true; }; - start(core: CoreStart) { + start(core: CoreStart, callResolveCluster: boolean) { const { http } = core; + + const hasESDataViaResolveIndex = async () => { + // fallback to previous implementation + const hasLocalESData = await this.checkLocalESData(http); + if (!hasLocalESData) { + const hasRemoteESData = await this.checkRemoteESData(http); + return hasRemoteESData; + } + return hasLocalESData; + }; + + const hasESDataViaResolveCluster = async () => { + try { + const { hasEsData } = await http.get<{ hasEsData: boolean }>( + '/internal/data_views/has_es_data', + { + version: '1', + } + ); + return hasEsData; + } catch (e) { + // fallback to previous implementation + return hasESDataViaResolveIndex(); + } + }; + return { /** * Check to see if ES data exists */ hasESData: async (): Promise => { - const hasLocalESData = await this.checkLocalESData(http); - if (!hasLocalESData) { - const hasRemoteESData = await this.checkRemoteESData(http); - return hasRemoteESData; + if (callResolveCluster) { + return hasESDataViaResolveCluster(); } - return hasLocalESData; + return hasESDataViaResolveIndex(); }, /** * Check to see if a data view exists diff --git a/src/plugins/data_views/server/rest_api_routes/internal/has_es_data.ts b/src/plugins/data_views/server/rest_api_routes/internal/has_es_data.ts new file mode 100644 index 0000000000000..6cd3e96ddfae6 --- /dev/null +++ b/src/plugins/data_views/server/rest_api_routes/internal/has_es_data.ts @@ -0,0 +1,58 @@ +/* + * 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 { IRouter, RequestHandlerContext } from '@kbn/core/server'; +import type { VersionedRoute } from '@kbn/core-http-server'; +import { schema } from '@kbn/config-schema'; +import { DEFAULT_ASSETS_TO_IGNORE } from '../../../common'; + +type Handler = Parameters['addVersion']>[1]; + +const patterns = ['*', '-.*'].concat( + DEFAULT_ASSETS_TO_IGNORE.DATA_STREAMS_TO_IGNORE.map((ds) => `-${ds}`) +); + +const crossClusterPatterns = patterns.map((ds) => `*:${ds}`); + +export const handler: Handler = async (ctx: RequestHandlerContext, req, res) => { + const core = await ctx.core; + const elasticsearchClient = core.elasticsearch.client.asCurrentUser; + const response = await elasticsearchClient.indices.resolveCluster({ + name: patterns.concat(crossClusterPatterns), + allow_no_indices: true, + ignore_unavailable: true, + }); + + const hasEsData = !!Object.values(response).find((cluster) => cluster.matching_indices); + + return res.ok({ body: { hasEsData } }); +}; + +export const registerHasEsDataRoute = (router: IRouter): void => { + router.versioned + .get({ + path: '/internal/data_views/has_es_data', + access: 'internal', + }) + .addVersion( + { + version: '1', + validate: { + response: { + 200: { + body: () => + schema.object({ + hasEsData: schema.boolean(), + }), + }, + }, + }, + }, + handler + ); +}; diff --git a/src/plugins/data_views/server/routes.ts b/src/plugins/data_views/server/routes.ts index d6ee36927ff80..54adfb0cf628d 100644 --- a/src/plugins/data_views/server/routes.ts +++ b/src/plugins/data_views/server/routes.ts @@ -14,6 +14,7 @@ import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies import { registerExistingIndicesPath } from './rest_api_routes/internal/existing_indices'; import { registerFieldForWildcard } from './rest_api_routes/internal/fields_for'; import { registerHasDataViewsRoute } from './rest_api_routes/internal/has_data_views'; +import { registerHasEsDataRoute } from './rest_api_routes/internal/has_es_data'; import { registerFields } from './rest_api_routes/internal/fields'; interface RegisterRoutesArgs { @@ -40,4 +41,5 @@ export function registerRoutes({ registerFieldForWildcard(router, getStartServices, isRollupsEnabled); registerFields(router, getStartServices, isRollupsEnabled); registerHasDataViewsRoute(router); + registerHasEsDataRoute(router); }