[data / data views] Disable rollup functionality when rollup plugin is disabled (#162674)

## Summary

Rollup functionality will be disabled on serverless instances. The
`rollup` plugin will be disabled. While it won't be possible to create
rollups on serverless, it will be possible to import data view saved
objects which may be configured for use with rollups. We will make a
'best effort' to improve functionality as to help users transition away
from rollups.

- In classic environments, the `rollup` plugin will enable `dataViews`
and `data` plugin rollup functionality.
- The data plugin will run an async search instead of a rollup search.
- The data views plugin will fetch normal fields, omitting a query for
rollup fields - which would fail anyway.

Closes https://github.com/elastic/kibana/issues/152708

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Matthew Kime 2023-08-08 11:25:13 -05:00 committed by GitHub
parent 874801bf7e
commit a69870b950
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 252 additions and 115 deletions

View file

@ -28,7 +28,6 @@
], ],
"optionalPlugins": [ "optionalPlugins": [
"usageCollection", "usageCollection",
"taskManager",
"security" "security"
], ],
"requiredBundles": [ "requiredBundles": [

View file

@ -12,10 +12,6 @@ import { BfetchServerSetup } from '@kbn/bfetch-plugin/server';
import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/server'; import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/server';
import type {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
import { ConfigSchema } from '../config'; import { ConfigSchema } from '../config';
import type { ISearchSetup, ISearchStart } from './search'; import type { ISearchSetup, ISearchStart } from './search';
@ -55,7 +51,6 @@ export interface DataPluginSetupDependencies {
expressions: ExpressionsServerSetup; expressions: ExpressionsServerSetup;
usageCollection?: UsageCollectionSetup; usageCollection?: UsageCollectionSetup;
fieldFormats: FieldFormatsSetup; fieldFormats: FieldFormatsSetup;
taskManager?: TaskManagerSetupContract;
security?: SecurityPluginSetup; security?: SecurityPluginSetup;
} }
@ -63,7 +58,6 @@ export interface DataPluginStartDependencies {
fieldFormats: FieldFormatsStart; fieldFormats: FieldFormatsStart;
logger: Logger; logger: Logger;
dataViews: DataViewsServerPluginStart; dataViews: DataViewsServerPluginStart;
taskManager?: TaskManagerStartContract;
} }
export class DataServerPlugin export class DataServerPlugin
@ -90,14 +84,7 @@ export class DataServerPlugin
public setup( public setup(
core: CoreSetup<DataPluginStartDependencies, DataPluginStart>, core: CoreSetup<DataPluginStartDependencies, DataPluginStart>,
{ { bfetch, expressions, usageCollection, fieldFormats, security }: DataPluginSetupDependencies
bfetch,
expressions,
usageCollection,
fieldFormats,
taskManager,
security,
}: DataPluginSetupDependencies
) { ) {
this.scriptsService.setup(core); this.scriptsService.setup(core);
const querySetup = this.queryService.setup(core); const querySetup = this.queryService.setup(core);
@ -110,7 +97,6 @@ export class DataServerPlugin
expressions, expressions,
usageCollection, usageCollection,
security, security,
taskManager,
}); });
return { return {
@ -120,14 +106,10 @@ export class DataServerPlugin
}; };
} }
public start( public start(core: CoreStart, { fieldFormats, dataViews }: DataPluginStartDependencies) {
core: CoreStart,
{ fieldFormats, dataViews, taskManager }: DataPluginStartDependencies
) {
const search = this.searchService.start(core, { const search = this.searchService.start(core, {
fieldFormats, fieldFormats,
indexPatterns: dataViews, indexPatterns: dataViews,
taskManager,
}); });
const datatableUtilities = new DatatableUtilitiesService( const datatableUtilities = new DatatableUtilitiesService(
search.aggs, search.aggs,

View file

@ -17,6 +17,7 @@ export function createSearchSetupMock(): jest.Mocked<ISearchSetup> {
aggs: searchAggsSetupMock(), aggs: searchAggsSetupMock(),
registerSearchStrategy: jest.fn(), registerSearchStrategy: jest.fn(),
searchSource: searchSourceMock.createSetupContract(), searchSource: searchSourceMock.createSetupContract(),
enableRollups: jest.fn(),
}; };
} }

View file

@ -25,10 +25,6 @@ import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { KbnServerError } from '@kbn/kibana-utils-plugin/server'; import { KbnServerError } from '@kbn/kibana-utils-plugin/server';
import type {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import type { import type {
@ -107,7 +103,6 @@ export interface SearchServiceSetupDependencies {
bfetch: BfetchServerSetup; bfetch: BfetchServerSetup;
expressions: ExpressionsServerSetup; expressions: ExpressionsServerSetup;
usageCollection?: UsageCollectionSetup; usageCollection?: UsageCollectionSetup;
taskManager?: TaskManagerSetupContract;
security?: SecurityPluginSetup; security?: SecurityPluginSetup;
} }
@ -115,7 +110,6 @@ export interface SearchServiceSetupDependencies {
export interface SearchServiceStartDependencies { export interface SearchServiceStartDependencies {
fieldFormats: FieldFormatsStart; fieldFormats: FieldFormatsStart;
indexPatterns: DataViewsServerPluginStart; indexPatterns: DataViewsServerPluginStart;
taskManager?: TaskManagerStartContract;
} }
/** @internal */ /** @internal */
@ -131,6 +125,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
private sessionService: SearchSessionService; private sessionService: SearchSessionService;
private asScoped!: ISearchStart['asScoped']; private asScoped!: ISearchStart['asScoped'];
private searchAsInternalUser!: ISearchStrategy; private searchAsInternalUser!: ISearchStrategy;
private rollupsEnabled: boolean = false;
constructor( constructor(
private initializerContext: PluginInitializerContext<ConfigSchema>, private initializerContext: PluginInitializerContext<ConfigSchema>,
@ -145,7 +140,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
public setup( public setup(
core: CoreSetup<DataPluginStartDependencies, DataPluginStart>, core: CoreSetup<DataPluginStartDependencies, DataPluginStart>,
{ bfetch, expressions, usageCollection, taskManager, security }: SearchServiceSetupDependencies { bfetch, expressions, usageCollection, security }: SearchServiceSetupDependencies
): ISearchSetup { ): ISearchSetup {
core.savedObjects.registerType(searchSessionSavedObjectType); core.savedObjects.registerType(searchSessionSavedObjectType);
const usage = usageCollection ? usageProvider(core) : undefined; const usage = usageCollection ? usageProvider(core) : undefined;
@ -261,12 +256,13 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
registerSearchStrategy: this.registerSearchStrategy, registerSearchStrategy: this.registerSearchStrategy,
usage, usage,
searchSource: this.searchSourceService.setup(), searchSource: this.searchSourceService.setup(),
enableRollups: () => (this.rollupsEnabled = true),
}; };
} }
public start( public start(
core: CoreStart, core: CoreStart,
{ fieldFormats, indexPatterns, taskManager }: SearchServiceStartDependencies { fieldFormats, indexPatterns }: SearchServiceStartDependencies
): ISearchStart { ): ISearchStart {
const { elasticsearch, savedObjects, uiSettings } = core; const { elasticsearch, savedObjects, uiSettings } = core;
@ -278,7 +274,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
indexPatterns, indexPatterns,
}); });
this.asScoped = this.asScopedProvider(core); this.asScoped = this.asScopedProvider(core, this.rollupsEnabled);
return { return {
aggs, aggs,
searchAsInternalUser: this.searchAsInternalUser, searchAsInternalUser: this.searchAsInternalUser,
@ -516,7 +512,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
return deps.searchSessionsClient.extend(sessionId, expires); return deps.searchSessionsClient.extend(sessionId, expires);
}; };
private asScopedProvider = (core: CoreStart) => { private asScopedProvider = (core: CoreStart, rollupsEnabled: boolean = false) => {
const { elasticsearch, savedObjects, uiSettings } = core; const { elasticsearch, savedObjects, uiSettings } = core;
const getSessionAsScoped = this.sessionService.asScopedProvider(core); const getSessionAsScoped = this.sessionService.asScopedProvider(core);
return (request: KibanaRequest): IScopedSearchClient => { return (request: KibanaRequest): IScopedSearchClient => {
@ -530,6 +526,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
uiSettings.asScopedToClient(savedObjectsClient) uiSettings.asScopedToClient(savedObjectsClient)
), ),
request, request,
rollupsEnabled,
}; };
return { return {
search: < search: <

View file

@ -64,6 +64,7 @@ describe('ES search strategy', () => {
}, },
}, },
searchSessionsClient: createSearchSessionsClientMock(), searchSessionsClient: createSearchSessionsClientMock(),
rollupsEnabled: true,
} as unknown as SearchStrategyDependencies; } as unknown as SearchStrategyDependencies;
const mockLegacyConfig$ = new BehaviorSubject<any>({ const mockLegacyConfig$ = new BehaviorSubject<any>({
elasticsearch: { elasticsearch: {
@ -233,6 +234,31 @@ describe('ES search strategy', () => {
expect(method).toBe('POST'); expect(method).toBe('POST');
expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); expect(path).toBe('/foo-%E7%A8%8B/_rollup_search');
}); });
it("doesn't call the rollup API if the index is a rollup type BUT rollups are disabled", async () => {
mockApiCaller.mockResolvedValueOnce(mockRollupResponse);
mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'foo-程', body: { query: {} } };
const esSearch = await enhancedEsSearchStrategyProvider(
mockLegacyConfig$,
mockSearchConfig,
mockLogger
);
await esSearch
.search(
{
indexType: 'rollup',
params,
},
{},
{ ...mockDeps, rollupsEnabled: false }
)
.toPromise();
expect(mockApiCaller).toBeCalledTimes(0);
});
}); });
describe('with sessionId', () => { describe('with sessionId', () => {

View file

@ -154,7 +154,7 @@ export const enhancedEsSearchStrategyProvider = (
throw new KbnServerError('Unknown indexType', 400); throw new KbnServerError('Unknown indexType', 400);
} }
if (request.indexType === undefined) { if (request.indexType === undefined || !deps.rollupsEnabled) {
return asyncSearch(request, options, deps); return asyncSearch(request, options, deps);
} else { } else {
return from(rollupSearch(request, options, deps)); return from(rollupSearch(request, options, deps));

View file

@ -35,6 +35,7 @@ export interface SearchStrategyDependencies {
uiSettingsClient: Pick<IUiSettingsClient, 'get'>; uiSettingsClient: Pick<IUiSettingsClient, 'get'>;
searchSessionsClient: IScopedSearchSessionsClient; searchSessionsClient: IScopedSearchSessionsClient;
request: KibanaRequest; request: KibanaRequest;
rollupsEnabled?: boolean;
} }
export interface ISearchSetup { export interface ISearchSetup {
@ -55,7 +56,7 @@ export interface ISearchSetup {
* Used internally for telemetry * Used internally for telemetry
*/ */
usage?: SearchUsage; usage?: SearchUsage;
enableRollups: () => void;
searchSource: ReturnType<SearchSourceService['setup']>; searchSource: ReturnType<SearchSourceService['setup']>;
} }

View file

@ -25,7 +25,6 @@
"@kbn/field-formats-plugin", "@kbn/field-formats-plugin",
"@kbn/data-views-plugin", "@kbn/data-views-plugin",
"@kbn/screenshot-mode-plugin", "@kbn/screenshot-mode-plugin",
"@kbn/task-manager-plugin",
"@kbn/security-plugin", "@kbn/security-plugin",
"@kbn/expressions-plugin", "@kbn/expressions-plugin",
"@kbn/field-types", "@kbn/field-types",

View file

@ -25,6 +25,7 @@ interface DataViewsServiceFactoryDeps {
uiSettings: UiSettingsServiceStart; uiSettings: UiSettingsServiceStart;
fieldFormats: FieldFormatsStart; fieldFormats: FieldFormatsStart;
capabilities: CoreStart['capabilities']; capabilities: CoreStart['capabilities'];
rollupsEnabled: boolean;
} }
/** /**
@ -38,14 +39,18 @@ export const dataViewsServiceFactory = (deps: DataViewsServiceFactoryDeps) =>
request?: KibanaRequest, request?: KibanaRequest,
byPassCapabilities?: boolean byPassCapabilities?: boolean
) { ) {
const { logger, uiSettings, fieldFormats, capabilities } = deps; const { logger, uiSettings, fieldFormats, capabilities, rollupsEnabled } = deps;
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient);
return new DataViewsService({ return new DataViewsService({
uiSettings: new UiSettingsServerToCommon(uiSettingsClient), uiSettings: new UiSettingsServerToCommon(uiSettingsClient),
savedObjectsClient: new SavedObjectsClientWrapper(savedObjectsClient), savedObjectsClient: new SavedObjectsClientWrapper(savedObjectsClient),
apiClient: new IndexPatternsApiServer(elasticsearchClient, savedObjectsClient), apiClient: new IndexPatternsApiServer(
elasticsearchClient,
savedObjectsClient,
rollupsEnabled
),
fieldFormats: formats, fieldFormats: formats,
onError: (error) => { onError: (error) => {
logger.error(error); logger.error(error);

View file

@ -10,6 +10,19 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { IndexPatternsFetcher } from '.'; import { IndexPatternsFetcher } from '.';
import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
const rollupResponse = {
foo: {
rollup_jobs: [
{
index_pattern: 'foo',
job_id: '123',
rollup_index: 'foo',
fields: [],
},
],
},
};
describe('Index Pattern Fetcher - server', () => { describe('Index Pattern Fetcher - server', () => {
let indexPatterns: IndexPatternsFetcher; let indexPatterns: IndexPatternsFetcher;
let esClient: ReturnType<typeof elasticsearchServiceMock.createElasticsearchClient>; let esClient: ReturnType<typeof elasticsearchServiceMock.createElasticsearchClient>;
@ -21,12 +34,40 @@ describe('Index Pattern Fetcher - server', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient = elasticsearchServiceMock.createElasticsearchClient();
indexPatterns = new IndexPatternsFetcher(esClient); indexPatterns = new IndexPatternsFetcher(esClient, false, true);
}); });
it('calls fieldcaps once', async () => { it('calls fieldcaps once', async () => {
esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse); esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse);
indexPatterns = new IndexPatternsFetcher(esClient, true); indexPatterns = new IndexPatternsFetcher(esClient, true, true);
await indexPatterns.getFieldsForWildcard({ pattern: patternList }); await indexPatterns.getFieldsForWildcard({ pattern: patternList });
expect(esClient.fieldCaps).toHaveBeenCalledTimes(1); expect(esClient.fieldCaps).toHaveBeenCalledTimes(1);
}); });
it('calls rollup api when given rollup data view', async () => {
esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse);
esClient.rollup.getRollupIndexCaps.mockResponse(
rollupResponse as unknown as estypes.RollupGetRollupIndexCapsResponse
);
indexPatterns = new IndexPatternsFetcher(esClient, true, true);
await indexPatterns.getFieldsForWildcard({
pattern: patternList,
type: 'rollup',
rollupIndex: 'foo',
});
expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(1);
});
it("doesn't call rollup api when given rollup data view and rollups are disabled", async () => {
esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse);
esClient.rollup.getRollupIndexCaps.mockResponse(
rollupResponse as unknown as estypes.RollupGetRollupIndexCapsResponse
);
indexPatterns = new IndexPatternsFetcher(esClient, true, false);
await indexPatterns.getFieldsForWildcard({
pattern: patternList,
type: 'rollup',
rollupIndex: 'foo',
});
expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(0);
});
}); });

View file

@ -40,10 +40,16 @@ interface FieldSubType {
export class IndexPatternsFetcher { export class IndexPatternsFetcher {
private elasticsearchClient: ElasticsearchClient; private elasticsearchClient: ElasticsearchClient;
private allowNoIndices: boolean; private allowNoIndices: boolean;
private rollupsEnabled: boolean;
constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices: boolean = false) { constructor(
elasticsearchClient: ElasticsearchClient,
allowNoIndices: boolean = false,
rollupsEnabled: boolean = false
) {
this.elasticsearchClient = elasticsearchClient; this.elasticsearchClient = elasticsearchClient;
this.allowNoIndices = allowNoIndices; this.allowNoIndices = allowNoIndices;
this.rollupsEnabled = rollupsEnabled;
} }
/** /**
@ -81,7 +87,7 @@ export class IndexPatternsFetcher {
fields: options.fields || ['*'], fields: options.fields || ['*'],
}); });
if (type === 'rollup' && rollupIndex) { if (this.rollupsEnabled && type === 'rollup' && rollupIndex) {
const rollupFields: FieldDescriptor[] = []; const rollupFields: FieldDescriptor[] = [];
const capabilityCheck = getCapabilitiesForRollupIndices( const capabilityCheck = getCapabilitiesForRollupIndices(
await this.elasticsearchClient.rollup.getRollupIndexCaps({ await this.elasticsearchClient.rollup.getRollupIndexCaps({

View file

@ -10,6 +10,7 @@ export { getFieldByName, findIndexPatternById } from './utils';
export type { FieldDescriptor, RollupIndexCapability } from './fetcher'; export type { FieldDescriptor, RollupIndexCapability } from './fetcher';
export { IndexPatternsFetcher, getCapabilitiesForRollupIndices } from './fetcher'; export { IndexPatternsFetcher, getCapabilitiesForRollupIndices } from './fetcher';
export type { export type {
DataViewsServerPluginSetup,
DataViewsServerPluginStart, DataViewsServerPluginStart,
DataViewsServerPluginSetupDependencies, DataViewsServerPluginSetupDependencies,
DataViewsServerPluginStartDependencies, DataViewsServerPluginStartDependencies,

View file

@ -16,7 +16,8 @@ export class IndexPatternsApiServer implements IDataViewsApiClient {
esClient: ElasticsearchClient; esClient: ElasticsearchClient;
constructor( constructor(
elasticsearchClient: ElasticsearchClient, elasticsearchClient: ElasticsearchClient,
private readonly savedObjectsClient: SavedObjectsClientContract private readonly savedObjectsClient: SavedObjectsClientContract,
private readonly rollupsEnabled: boolean
) { ) {
this.esClient = elasticsearchClient; this.esClient = elasticsearchClient;
} }
@ -29,7 +30,11 @@ export class IndexPatternsApiServer implements IDataViewsApiClient {
indexFilter, indexFilter,
fields, fields,
}: GetFieldsOptions) { }: GetFieldsOptions) {
const indexPatterns = new IndexPatternsFetcher(this.esClient, allowNoIndex); const indexPatterns = new IndexPatternsFetcher(
this.esClient,
allowNoIndex,
this.rollupsEnabled
);
return await indexPatterns return await indexPatterns
.getFieldsForWildcard({ .getFieldsForWildcard({
pattern, pattern,

View file

@ -33,6 +33,7 @@ export class DataViewsServerPlugin
> >
{ {
private readonly logger: Logger; private readonly logger: Logger;
private rollupsEnabled: boolean = false;
constructor(initializerContext: PluginInitializerContext) { constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get('dataView'); this.logger = initializerContext.logger.get('dataView');
@ -46,7 +47,12 @@ export class DataViewsServerPlugin
core.capabilities.registerProvider(capabilitiesProvider); core.capabilities.registerProvider(capabilitiesProvider);
const dataViewRestCounter = usageCollection?.createUsageCounter('dataViewsRestApi'); const dataViewRestCounter = usageCollection?.createUsageCounter('dataViewsRestApi');
registerRoutes(core.http, core.getStartServices, dataViewRestCounter); registerRoutes(
core.http,
core.getStartServices,
() => this.rollupsEnabled,
dataViewRestCounter
);
expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices }));
registerIndexPatternsUsageCollector(core.getStartServices, usageCollection); registerIndexPatternsUsageCollector(core.getStartServices, usageCollection);
@ -60,7 +66,9 @@ export class DataViewsServerPlugin
}, },
}); });
return {}; return {
enableRollups: () => (this.rollupsEnabled = true),
};
} }
public start( public start(
@ -72,6 +80,7 @@ export class DataViewsServerPlugin
uiSettings, uiSettings,
fieldFormats, fieldFormats,
capabilities, capabilities,
rollupsEnabled: this.rollupsEnabled,
}); });
return { return {

View file

@ -111,86 +111,93 @@ const validate: FullValidationConfig<any, any, any> = {
}, },
}; };
const handler: RequestHandler<{}, IQuery, IBody> = async (context, request, response) => { const handler: (isRollupsEnabled: () => boolean) => RequestHandler<{}, IQuery, IBody> =
const { asCurrentUser } = (await context.core).elasticsearch.client; (isRollupsEnabled) => async (context, request, response) => {
const indexPatterns = new IndexPatternsFetcher(asCurrentUser); const { asCurrentUser } = (await context.core).elasticsearch.client;
const { const indexPatterns = new IndexPatternsFetcher(asCurrentUser, undefined, isRollupsEnabled());
pattern, const {
meta_fields: metaFields,
type,
rollup_index: rollupIndex,
allow_no_index: allowNoIndex,
include_unmapped: includeUnmapped,
} = request.query;
// not available to get request
const indexFilter = request.body?.index_filter;
let parsedFields: string[] = [];
let parsedMetaFields: string[] = [];
try {
parsedMetaFields = parseFields(metaFields);
parsedFields = parseFields(request.query.fields ?? []);
} catch (error) {
return response.badRequest();
}
try {
const { fields, indices } = await indexPatterns.getFieldsForWildcard({
pattern, pattern,
metaFields: parsedMetaFields, meta_fields: metaFields,
type, type,
rollupIndex, rollup_index: rollupIndex,
fieldCapsOptions: { allow_no_index: allowNoIndex,
allow_no_indices: allowNoIndex || false, include_unmapped: includeUnmapped,
includeUnmapped, } = request.query;
},
indexFilter,
...(parsedFields.length > 0 ? { fields: parsedFields } : {}),
});
const body: { fields: FieldDescriptorRestResponse[]; indices: string[] } = { fields, indices }; // not available to get request
const indexFilter = request.body?.index_filter;
return response.ok({ let parsedFields: string[] = [];
body, let parsedMetaFields: string[] = [];
headers: { try {
'content-type': 'application/json', parsedMetaFields = parseFields(metaFields);
}, parsedFields = parseFields(request.query.fields ?? []);
}); } catch (error) {
} catch (error) { return response.badRequest();
if ( }
typeof error === 'object' &&
!!error?.isBoom && try {
!!error?.output?.payload && const { fields, indices } = await indexPatterns.getFieldsForWildcard({
typeof error?.output?.payload === 'object' pattern,
) { metaFields: parsedMetaFields,
const payload = error?.output?.payload; type,
return response.notFound({ rollupIndex,
body: { fieldCapsOptions: {
message: payload.message, allow_no_indices: allowNoIndex || false,
attributes: payload, includeUnmapped,
},
indexFilter,
...(parsedFields.length > 0 ? { fields: parsedFields } : {}),
});
const body: { fields: FieldDescriptorRestResponse[]; indices: string[] } = {
fields,
indices,
};
return response.ok({
body,
headers: {
'content-type': 'application/json',
}, },
}); });
} else { } catch (error) {
return response.notFound(); if (
typeof error === 'object' &&
!!error?.isBoom &&
!!error?.output?.payload &&
typeof error?.output?.payload === 'object'
) {
const payload = error?.output?.payload;
return response.notFound({
body: {
message: payload.message,
attributes: payload,
},
});
} else {
return response.notFound();
}
} }
} };
};
export const registerFieldForWildcard = ( export const registerFieldForWildcard = async (
router: IRouter, router: IRouter,
getStartServices: StartServicesAccessor< getStartServices: StartServicesAccessor<
DataViewsServerPluginStartDependencies, DataViewsServerPluginStartDependencies,
DataViewsServerPluginStart DataViewsServerPluginStart
> >,
isRollupsEnabled: () => boolean
) => { ) => {
const configuredHandler = handler(isRollupsEnabled);
// handler // handler
router.versioned.put({ path, access }).addVersion({ version, validate }, handler); router.versioned.put({ path, access }).addVersion({ version, validate }, configuredHandler);
router.versioned.post({ path, access }).addVersion({ version, validate }, handler); router.versioned.post({ path, access }).addVersion({ version, validate }, configuredHandler);
router.versioned router.versioned
.get({ path, access }) .get({ path, access })
.addVersion( .addVersion(
{ version, validate: { request: { query: querySchema }, response: validate.response } }, { version, validate: { request: { query: querySchema }, response: validate.response } },
handler configuredHandler
); );
}; };

View file

@ -20,12 +20,13 @@ export function registerRoutes(
DataViewsServerPluginStartDependencies, DataViewsServerPluginStartDependencies,
DataViewsServerPluginStart DataViewsServerPluginStart
>, >,
isRollupsEnabled: () => boolean,
dataViewRestCounter?: UsageCounter dataViewRestCounter?: UsageCounter
) { ) {
const router = http.createRouter(); const router = http.createRouter();
routes.forEach((route) => route(router, getStartServices, dataViewRestCounter)); routes.forEach((route) => route(router, getStartServices, dataViewRestCounter));
registerFieldForWildcard(router, getStartServices); registerFieldForWildcard(router, getStartServices, isRollupsEnabled);
registerHasDataViewsRoute(router); registerHasDataViewsRoute(router);
} }

View file

@ -53,8 +53,9 @@ export interface DataViewsServerPluginStart {
/** /**
* DataViews server plugin setup api * DataViews server plugin setup api
*/ */
// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DataViewsServerPluginSetup {
export interface DataViewsServerPluginSetup {} enableRollups: () => void;
}
/** /**
* Data Views server setup dependencies * Data Views server setup dependencies

View file

@ -298,6 +298,7 @@ const createSearchStrategyDependenciesMock = (): SearchStrategyDependencies => (
savedObjectsClient: savedObjectsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(),
searchSessionsClient: createSearchSessionsClientMock(), searchSessionsClient: createSearchSessionsClientMock(),
request: httpServerMock.createKibanaRequest(), request: httpServerMock.createKibanaRequest(),
rollupsEnabled: true,
}); });
// using the official data mock from within x-pack doesn't type-check successfully, // using the official data mock from within x-pack doesn't type-check successfully,

View file

@ -13,13 +13,14 @@
"requiredPlugins": [ "requiredPlugins": [
"management", "management",
"licensing", "licensing",
"features" "features",
"dataViews",
"data"
], ],
"optionalPlugins": [ "optionalPlugins": [
"home", "home",
"indexManagement", "indexManagement",
"usageCollection", "usageCollection"
"visTypeTimeseries"
], ],
"requiredBundles": [ "requiredBundles": [
"kibanaUtils", "kibanaUtils",

View file

@ -30,8 +30,8 @@ export class RollupPlugin implements Plugin<void, void, any, any> {
} }
public setup( public setup(
{ http, uiSettings, savedObjects, getStartServices }: CoreSetup, { http, uiSettings, getStartServices }: CoreSetup,
{ features, licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies { features, licensing, indexManagement, usageCollection, dataViews, data }: Dependencies
) { ) {
this.license.setup( this.license.setup(
{ {
@ -103,6 +103,8 @@ export class RollupPlugin implements Plugin<void, void, any, any> {
if (indexManagement && indexManagement.indexDataEnricher) { if (indexManagement && indexManagement.indexDataEnricher) {
indexManagement.indexDataEnricher.add(rollupDataEnricher); indexManagement.indexDataEnricher.add(rollupDataEnricher);
} }
dataViews.enableRollups();
data.search.enableRollups();
} }
start() {} start() {}

View file

@ -12,6 +12,8 @@ import { VisTypeTimeseriesSetup } from '@kbn/vis-type-timeseries-plugin/server';
import { getCapabilitiesForRollupIndices } from '@kbn/data-plugin/server'; import { getCapabilitiesForRollupIndices } from '@kbn/data-plugin/server';
import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/server'; import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/server';
import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
import { DataViewsServerPluginSetup } from '@kbn/data-views-plugin/server';
import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server';
import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
import { License } from './services'; import { License } from './services';
import { IndexPatternsFetcher } from './shared_imports'; import { IndexPatternsFetcher } from './shared_imports';
@ -24,6 +26,8 @@ export interface Dependencies {
usageCollection?: UsageCollectionSetup; usageCollection?: UsageCollectionSetup;
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
features: FeaturesPluginSetup; features: FeaturesPluginSetup;
dataViews: DataViewsServerPluginSetup;
data: DataPluginSetup;
} }
export interface RouteDependencies { export interface RouteDependencies {

View file

@ -32,6 +32,7 @@
"@kbn/i18n-react", "@kbn/i18n-react",
"@kbn/config-schema", "@kbn/config-schema",
"@kbn/shared-ux-router", "@kbn/shared-ux-router",
"@kbn/data-views-plugin",
], ],
"exclude": [ "exclude": [

View file

@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./security_users')); loadTestFile(require.resolve('./security_users'));
loadTestFile(require.resolve('./spaces')); loadTestFile(require.resolve('./spaces'));
loadTestFile(require.resolve('./security_response_headers')); loadTestFile(require.resolve('./security_response_headers'));
loadTestFile(require.resolve('./rollups'));
}); });
} }

View file

@ -0,0 +1,44 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from 'expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants';
import { X_ELASTIC_INTERNAL_ORIGIN_REQUEST } from '@kbn/core-http-common/src/constants';
import { FIELDS_FOR_WILDCARD_PATH as BASE_URI } from '@kbn/data-views-plugin/common/constants';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('rollup data views - fields for wildcard', function () {
before(async () => {
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('returns 200 and best effort response despite lack of rollup support', async () => {
const response = await supertest
.get(BASE_URI)
.query({
pattern: 'basic_index',
type: 'rollup',
rollup_index: 'bar',
})
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL)
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'true');
expect(response.status).toBe(200);
expect(response.body.fields.length).toEqual(5);
});
});
}

View file

@ -44,5 +44,7 @@
"@kbn/fleet-plugin", "@kbn/fleet-plugin",
"@kbn/cases-plugin", "@kbn/cases-plugin",
"@kbn/test-subj-selector", "@kbn/test-subj-selector",
"@kbn/core-http-common",
"@kbn/data-views-plugin",
] ]
} }