[Enterprise Search] Add override pattern for fetch_indices (#138765) (#138854)

* Add override pattern for fetch_indices

* Add tests for fetch_indices override pattern

* Include only aliases.

* Add negative check to the tests and added some comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 64c1d3cb2b)

Co-authored-by: Efe Gürkan YALAMAN <efeguerkan.yalaman@elastic.co>
This commit is contained in:
Kibana Machine 2022-08-15 18:15:09 -04:00 committed by GitHub
parent 69dfad2689
commit 90ee18f0e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 324 additions and 28 deletions

View file

@ -0,0 +1,126 @@
/*
* 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.
*/
export const mockSingleIndexResponse = {
'search-regular-index': {
aliases: {},
},
};
export const mockSingleIndexStatsResponse = {
indices: {
'search-regular-index': {
health: 'green',
status: 'open',
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: 108000,
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
},
};
export const mockMultiIndexResponse = {
'hidden-index': {
aliases: {
'alias-hidden-index': {},
'search-alias-hidden-index': {},
},
settings: { index: { hidden: 'true' } },
},
'regular-index': {
aliases: {
'alias-regular-index': {},
'search-alias-regular-index': {},
},
},
'search-prefixed-hidden-index': {
aliases: {
'alias-search-prefixed-hidden-index': {},
'search-alias-search-prefixed-hidden-index': {},
},
settings: { index: { hidden: 'true' } },
},
'search-prefixed-regular-index': {
aliases: {
'alias-search-prefixed-regular-index': {},
'search-alias-search-prefixed-regular-index': {},
},
},
};
export const mockMultiStatsResponse: {
indices: Record<string, { total: object; [k: string]: unknown }>;
} = {
indices: {
'alias-hidden-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'alias-regular-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'alias-search-prefixed-hidden-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'alias-search-prefixed-regular-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'hidden-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'regular-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'search-alias-hidden-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'search-alias-regular-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'search-alias-search-prefixed-hidden-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'search-alias-search-prefixed-regular-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'search-prefixed-hidden-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
'search-prefixed-regular-index': {
...mockSingleIndexStatsResponse.indices['search-regular-index'],
},
},
};
export const mockPrivilegesResponse = Object.keys(mockMultiStatsResponse.indices).reduce<
Record<string, unknown>
>((acc, key) => {
acc[key] = { manage: true, read: true };
return acc;
}, {});
export const getIndexReturnValue = (indexName: string) => {
return {
...mockMultiStatsResponse.indices[indexName],
alias: indexName.startsWith('alias') || indexName.startsWith('search-alias'),
count: 100,
name: indexName,
privileges: { manage: true, read: true },
total: {
...mockMultiStatsResponse.indices[indexName].total,
store: {
size_in_bytes: '105.47kb',
},
},
};
};

View file

@ -5,6 +5,13 @@
* 2.0.
*/
import {
getIndexReturnValue,
mockMultiIndexResponse,
mockMultiStatsResponse,
mockPrivilegesResponse,
} from '../../__mocks__/fetch_indices.mock';
import { ByteSizeValue } from '@kbn/config-schema';
import { IScopedClusterClient } from '@kbn/core/server';
@ -13,6 +20,7 @@ import { fetchIndices } from './fetch_indices';
describe('fetchIndices lib function', () => {
const mockClient = {
asCurrentUser: {
count: jest.fn().mockReturnValue({ count: 100 }),
indices: {
get: jest.fn(),
stats: jest.fn(),
@ -20,7 +28,6 @@ describe('fetchIndices lib function', () => {
security: {
hasPrivileges: jest.fn(),
},
count: jest.fn().mockReturnValue({ count: 100 }),
},
asInternalUser: {},
};
@ -53,11 +60,11 @@ describe('fetchIndices lib function', () => {
mockClient.asCurrentUser.security.hasPrivileges.mockImplementation(() => ({
index: {
'index-without-prefix': { read: true, manage: true },
'search-aliased': { read: true, manage: true },
'search-double-aliased': { read: true, manage: true },
'search-regular-index': { read: true, manage: true },
'second-index': { read: true, manage: true },
'index-without-prefix': { manage: true, read: true },
'search-aliased': { manage: true, read: true },
'search-double-aliased': { manage: true, read: true },
'search-regular-index': { manage: true, read: true },
'second-index': { manage: true, read: true },
},
}));
@ -76,12 +83,12 @@ describe('fetchIndices lib function', () => {
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', false, true)
).resolves.toEqual([
{
alias: false,
count: 100,
health: 'green',
name: 'search-regular-index',
privileges: { manage: true, read: true },
status: 'open',
alias: false,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
@ -125,12 +132,12 @@ describe('fetchIndices lib function', () => {
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', true, true)
).resolves.toEqual([
{
alias: false,
count: 100,
health: 'green',
name: 'search-regular-index',
privileges: { manage: true, read: true },
status: 'open',
alias: false,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
@ -368,4 +375,113 @@ describe('fetchIndices lib function', () => {
).resolves.toEqual([]);
expect(mockClient.asCurrentUser.indices.stats).not.toHaveBeenCalled();
});
describe('alwaysShowSearchPattern', () => {
beforeEach(() => {
mockClient.asCurrentUser.indices.get.mockImplementation(() => mockMultiIndexResponse);
mockClient.asCurrentUser.indices.stats.mockImplementation(() => mockMultiStatsResponse);
mockClient.asCurrentUser.security.hasPrivileges.mockImplementation(() => ({
index: mockPrivilegesResponse,
}));
});
it('overrides hidden indices setting', async () => {
const returnValue = await fetchIndices(
mockClient as unknown as IScopedClusterClient,
'*',
false,
true,
'search-'
);
// This is the list of mock indices and aliases that are:
// - Non-hidden indices and aliases
// - search- prefixed aliases that point to hidden indices
expect(returnValue).toEqual(
[
'regular-index',
'alias-regular-index',
'search-alias-regular-index',
'search-prefixed-regular-index',
'alias-search-prefixed-regular-index',
'search-alias-search-prefixed-regular-index',
'search-alias-hidden-index',
'search-alias-search-prefixed-hidden-index',
].map(getIndexReturnValue)
);
// This is the list of mock indices and aliases that are:
// - Hidden indices
// - aliases to hidden indices that has no prefix
expect(returnValue).toEqual(
expect.not.arrayContaining(
[
'hidden-index',
'search-prefixed-hidden-index',
'alias-hidden-index',
'alias-search-prefixed-hidden-index',
].map(getIndexReturnValue)
)
);
expect(mockClient.asCurrentUser.indices.get).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
features: ['aliases', 'settings'],
filter_path: ['*.aliases', '*.settings.index.hidden'],
index: '*',
});
expect(mockClient.asCurrentUser.indices.stats).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
index: '*',
metric: ['docs', 'store'],
});
expect(mockClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({
index: [
{
names: expect.arrayContaining(Object.keys(mockMultiStatsResponse.indices)),
privileges: ['read', 'manage'],
},
],
});
});
it('returns everything if hidden indices set', async () => {
const returnValue = await fetchIndices(
mockClient as unknown as IScopedClusterClient,
'*',
true,
true,
'search-'
);
expect(returnValue).toEqual(
expect.arrayContaining(Object.keys(mockMultiStatsResponse.indices).map(getIndexReturnValue))
);
expect(mockClient.asCurrentUser.indices.get).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
features: ['aliases', 'settings'],
filter_path: ['*.aliases', '*.settings.index.hidden'],
index: '*',
});
expect(mockClient.asCurrentUser.indices.stats).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
index: '*',
metric: ['docs', 'store'],
});
expect(mockClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({
index: [
{
names: expect.arrayContaining(Object.keys(mockMultiStatsResponse.indices)),
privileges: ['read', 'manage'],
},
],
});
});
});
});

View file

@ -59,10 +59,13 @@ export const fetchIndices = async (
client: IScopedClusterClient,
indexPattern: string,
returnHiddenIndices: boolean,
includeAliases: boolean
includeAliases: boolean,
alwaysShowSearchPattern?: 'search-'
): Promise<ElasticsearchIndexWithPrivileges[]> => {
// This call retrieves alias and settings information about indices
const expandWildcards: ExpandWildcard[] = returnHiddenIndices ? ['hidden', 'all'] : ['open'];
// If we provide an override pattern with alwaysShowSearchPattern we get everything and filter out hiddens.
const expandWildcards: ExpandWildcard[] =
returnHiddenIndices || alwaysShowSearchPattern ? ['hidden', 'all'] : ['open'];
const totalIndices = await client.asCurrentUser.indices.get({
expand_wildcards: expandWildcards,
// for better performance only compute aliases and settings of indices but not mappings
@ -73,12 +76,22 @@ export const fetchIndices = async (
index: indexPattern,
});
// Index names that with one of their aliases match with the alwaysShowSearchPattern
const alwaysShowPatternMatches = new Set<string>();
const indexAndAliasNames = Object.keys(totalIndices).reduce((accum, indexName) => {
accum.push(indexName);
if (includeAliases) {
const aliases = Object.keys(totalIndices[indexName].aliases!);
aliases.forEach((alias) => accum.push(alias));
aliases.forEach((alias) => {
accum.push(alias);
// Add indexName to the set if an alias matches the pattern
if (alwaysShowSearchPattern && alias.startsWith(alwaysShowSearchPattern)) {
alwaysShowPatternMatches.add(indexName);
}
});
}
return accum;
}, [] as string[]);
@ -110,7 +123,36 @@ export const fetchIndices = async (
const indexCounts = await fetchIndexCounts(client, indexAndAliasNames);
return indicesNames
// Index data to show even if they are hidden, set by alwaysShowSearchPattern
const alwaysShowIndices = alwaysShowSearchPattern
? Array.from(alwaysShowPatternMatches)
.map((indexName: string) => {
const indexData = totalIndices[indexName];
const indexStats = indicesStats[indexName];
return mapIndexStats(indexData, indexStats, indexName);
})
.flatMap(({ name, aliases, ...indexData }) => {
const indicesAndAliases = [] as ElasticsearchIndexWithPrivileges[];
if (includeAliases) {
aliases.forEach((alias) => {
if (alias.startsWith(alwaysShowSearchPattern)) {
indicesAndAliases.push({
alias: true,
count: indexCounts[alias] ?? 0,
name: alias,
privileges: { manage: false, read: false, ...indexPrivileges[name] },
...indexData,
});
}
});
}
return indicesAndAliases;
})
: [];
const regularIndexData = indicesNames
.map((indexName: string) => {
const indexData = totalIndices[indexName];
const indexStats = indicesStats[indexName];
@ -120,30 +162,42 @@ export const fetchIndices = async (
// expand aliases and add to results
const indicesAndAliases = [] as ElasticsearchIndexWithPrivileges[];
indicesAndAliases.push({
name,
count: indexCounts[name] ?? 0,
alias: false,
privileges: { read: false, manage: false, ...indexPrivileges[name] },
count: indexCounts[name] ?? 0,
name,
privileges: { manage: false, read: false, ...indexPrivileges[name] },
...indexData,
});
if (includeAliases) {
aliases.forEach((alias) => {
indicesAndAliases.push({
name: alias,
count: indexCounts[alias] ?? 0,
alias: true,
privileges: { read: false, manage: false, ...indexPrivileges[name] },
count: indexCounts[alias] ?? 0,
name: alias,
privileges: { manage: false, read: false, ...indexPrivileges[name] },
...indexData,
});
});
}
return indicesAndAliases;
})
.filter(
({ name }, index, array) =>
// make list of aliases unique since we add an alias per index above
// and aliases can point to multiple indices
array.findIndex((engineData) => engineData.name === name) === index
);
});
const indexNamesAlreadyIncluded = regularIndexData.map(({ name }) => name);
const indexNamesToInclude = alwaysShowIndices
.map(({ name }) => name)
.filter((name) => !indexNamesAlreadyIncluded.includes(name));
const itemsToInclude = alwaysShowIndices.filter(({ name }) => indexNamesToInclude.includes(name));
const indicesData = alwaysShowSearchPattern
? ([...regularIndexData, ...itemsToInclude] as ElasticsearchIndexWithPrivileges[])
: regularIndexData;
return indicesData.filter(
({ name }, index, array) =>
// make list of aliases unique since we add an alias per index above
// and aliases can point to multiple indices
array.findIndex((engineData) => engineData.name === name) === index
);
};

View file

@ -27,7 +27,7 @@ export function registerIndexRoutes({ router, log }: RouteDependencies) {
{ path: '/internal/enterprise_search/search_indices', validate: false },
elasticsearchErrorHandler(log, async (context, _, response) => {
const { client } = (await context.core).elasticsearch;
const indices = await fetchIndices(client, '*', false, true);
const indices = await fetchIndices(client, '*', false, true, 'search-');
return response.ok({
body: indices,