diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap index 4fea46169cc5b..7e90618079021 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap @@ -100,7 +100,7 @@ exports[`extend index management ilm summary extension should return extension w index={ Object { "aliases": "none", - "documents": "2", + "documents": 2, "documents_deleted": "0", "health": "yellow", "hidden": false, @@ -137,9 +137,9 @@ exports[`extend index management ilm summary extension should return extension w }, "isFrozen": false, "name": "testy3", - "primary": "1", + "primary": 1, "primary_size": "6.5kb", - "replica": "1", + "replica": 1, "size": "6.5kb", "status": "open", "uuid": "XL11TLa3Tvq298_dMUzLHQ", @@ -648,7 +648,7 @@ exports[`extend index management ilm summary extension should return extension w index={ Object { "aliases": "none", - "documents": "2", + "documents": 2, "documents_deleted": "0", "health": "yellow", "hidden": false, @@ -666,9 +666,9 @@ exports[`extend index management ilm summary extension should return extension w }, "isFrozen": false, "name": "testy3", - "primary": "1", + "primary": 1, "primary_size": "6.5kb", - "replica": "1", + "replica": 1, "size": "6.5kb", "status": "open", "uuid": "XL11TLa3Tvq298_dMUzLHQ", diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index 31b43cb67d934..a38c6e1af5c2a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -41,9 +41,9 @@ const indexWithoutLifecyclePolicy: Index = { status: 'open', name: 'noPolicy', uuid: 'BJ-nrZYuSrG-jaofr6SeLg', - primary: '1', - replica: '1', - documents: '1', + primary: 1, + replica: 1, + documents: 1, documents_deleted: '0', size: '3.4kb', primary_size: '3.4kb', @@ -61,9 +61,9 @@ const indexWithLifecyclePolicy: Index = { status: 'open', name: 'testy3', uuid: 'XL11TLa3Tvq298_dMUzLHQ', - primary: '1', - replica: '1', - documents: '2', + primary: 1, + replica: 1, + documents: 2, documents_deleted: '0', size: '6.5kb', primary_size: '6.5kb', @@ -89,9 +89,9 @@ const indexWithLifecycleError = { status: 'open', name: 'testy3', uuid: 'XL11TLa3Tvq298_dMUzLHQ', - primary: '1', - replica: '1', - documents: '2', + primary: 1, + replica: 1, + documents: 2, documents_deleted: '0', size: '6.5kb', primary_size: '6.5kb', diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index 375632942b3dd..28877dee77e57 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -52,14 +52,14 @@ export interface IndexSettings { } export interface Index { - health: string; - status: string; + health?: string; + status?: string; name: string; - uuid: string; - primary: string; - replica: string; - documents?: string; - size: any; + uuid?: string; + primary?: number | string; + replica?: number | string; + documents: number; + size: string; isFrozen: boolean; hidden: boolean; aliases: string | string[]; diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts new file mode 100644 index 0000000000000..759819d8cb1fa --- /dev/null +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts @@ -0,0 +1,185 @@ +/* + * 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 { RequestMock, routeDependencies, RouterMock } from '../test/helpers'; +import { addBasePath } from '../routes/api'; +import { registerIndicesRoutes } from '../routes/api/indices'; +import { + createTestIndexResponse, + createTestIndexState, + createTestIndexStats, +} from '../test/helpers/indices_fixtures'; + +describe('[Index management API Routes] fetch indices lib function', () => { + const router = new RouterMock(); + + const getIndices = router.getMockESApiFn('indices.get'); + const getIndicesStats = router.getMockESApiFn('indices.stats'); + const mockRequest: RequestMock = { + method: 'get', + path: addBasePath('/indices'), + }; + + beforeAll(() => { + registerIndicesRoutes({ + ...routeDependencies, + router, + }); + }); + + test('regular index', async () => { + getIndices.mockResolvedValue({ + body: { + regular_index: createTestIndexState(), + }, + }); + getIndicesStats.mockResolvedValue({ + body: { + indices: { + regular_index: createTestIndexStats({ uuid: 'regular_index' }), + }, + }, + }); + + await expect(router.runRequest(mockRequest)).resolves.toEqual({ + body: [createTestIndexResponse({ name: 'regular_index', uuid: 'regular_index' })], + }); + }); + test('index with aliases', async () => { + getIndices.mockResolvedValue({ + body: { + index_with_aliases: createTestIndexState({ + aliases: { test_alias: {}, another_alias: {} }, + }), + }, + }); + getIndicesStats.mockResolvedValue({ + body: { + indices: { + index_with_aliases: createTestIndexStats({ uuid: 'index_with_aliases' }), + }, + }, + }); + + await expect(router.runRequest(mockRequest)).resolves.toEqual({ + body: [ + createTestIndexResponse({ + aliases: ['test_alias', 'another_alias'], + name: 'index_with_aliases', + uuid: 'index_with_aliases', + }), + ], + }); + }); + test('frozen index', async () => { + getIndices.mockResolvedValue({ + body: { + frozen_index: createTestIndexState({ + // @ts-expect-error + settings: { index: { number_of_shards: 1, number_of_replicas: 1, frozen: 'true' } }, + }), + }, + }); + getIndicesStats.mockResolvedValue({ + body: { + indices: { + frozen_index: createTestIndexStats({ uuid: 'frozen_index' }), + }, + }, + }); + + await expect(router.runRequest(mockRequest)).resolves.toEqual({ + body: [ + createTestIndexResponse({ + name: 'frozen_index', + uuid: 'frozen_index', + isFrozen: true, + }), + ], + }); + }); + test('hidden index', async () => { + getIndices.mockResolvedValue({ + body: { + hidden_index: createTestIndexState({ + settings: { index: { number_of_shards: 1, number_of_replicas: 1, hidden: 'true' } }, + }), + }, + }); + getIndicesStats.mockResolvedValue({ + body: { + indices: { + hidden_index: createTestIndexStats({ uuid: 'hidden_index' }), + }, + }, + }); + + await expect(router.runRequest(mockRequest)).resolves.toEqual({ + body: [ + createTestIndexResponse({ + name: 'hidden_index', + uuid: 'hidden_index', + hidden: true, + }), + ], + }); + }); + test('data stream index', async () => { + getIndices.mockResolvedValue({ + body: { + data_stream_index: createTestIndexState({ + data_stream: 'test_data_stream', + }), + }, + }); + getIndicesStats.mockResolvedValue({ + body: { + indices: { + data_stream_index: createTestIndexStats({ uuid: 'data_stream_index' }), + }, + }, + }); + + await expect(router.runRequest(mockRequest)).resolves.toEqual({ + body: [ + createTestIndexResponse({ + name: 'data_stream_index', + uuid: 'data_stream_index', + data_stream: 'test_data_stream', + }), + ], + }); + }); + test('index missing in stats call', async () => { + getIndices.mockResolvedValue({ + body: { + index_missing_stats: createTestIndexState(), + }, + }); + // simulates when an index has been deleted after get indices call + // deleted index won't be present in the indices stats call response + getIndicesStats.mockResolvedValue({ + body: { + indices: { + some_other_index: createTestIndexStats({ uuid: 'some_other_index' }), + }, + }, + }); + await expect(router.runRequest(mockRequest)).resolves.toEqual({ + body: [ + createTestIndexResponse({ + name: 'index_missing_stats', + uuid: undefined, + health: undefined, + status: undefined, + documents: 0, + size: '0b', + }), + ], + }); + }); +}); diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts index 84d9897cc3392..5050353f992b6 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ByteSizeValue } from '@kbn/config-schema'; import { IScopedClusterClient } from 'kibana/server'; import { IndexDataEnricher } from '../services'; import { Index } from '../index'; @@ -19,49 +20,53 @@ async function fetchIndicesCall( const { body: indices } = await client.asCurrentUser.indices.get({ index: indexNamesString, expand_wildcards: ['hidden', 'all'], - // only get specified properties in the response - filter_path: ['*.aliases', '*.settings.index.hidden', '*.data_stream'], + // only get specified index properties from ES to keep the response under 536MB + // node.js string length limit: https://github.com/nodejs/node/issues/33960 + filter_path: [ + '*.aliases', + '*.settings.index.number_of_shards', + '*.settings.index.number_of_replicas', + '*.settings.index.frozen', + '*.settings.index.hidden', + '*.data_stream', + ], }); if (!Object.keys(indices).length) { return []; } - const { body: catHits } = await client.asCurrentUser.cat.indices({ - format: 'json', - h: 'health,status,index,uuid,pri,rep,docs.count,sth,store.size', - expand_wildcards: ['hidden', 'all'], + const { + body: { indices: indicesStats = {} }, + } = await client.asCurrentUser.indices.stats({ index: indexNamesString, + expand_wildcards: ['hidden', 'all'], + forbid_closed_indices: false, + metric: ['docs', 'store'], + }); + const indicesNames = Object.keys(indices); + return indicesNames.map((indexName: string) => { + const indexData = indices[indexName]; + const indexStats = indicesStats[indexName]; + const aliases = Object.keys(indexData.aliases!); + return { + // @ts-expect-error new property https://github.com/elastic/elasticsearch-specification/issues/1253 + health: indexStats?.health, + // @ts-expect-error new property https://github.com/elastic/elasticsearch-specification/issues/1253 + status: indexStats?.status, + name: indexName, + uuid: indexStats?.uuid, + primary: indexData.settings?.index?.number_of_shards, + replica: indexData.settings?.index?.number_of_replicas, + documents: indexStats?.total?.docs?.count ?? 0, + size: new ByteSizeValue(indexStats?.total?.store?.size_in_bytes ?? 0).toString(), + // @ts-expect-error + isFrozen: indexData.settings?.index?.frozen === 'true', + aliases: aliases.length ? aliases : 'none', + hidden: indexData.settings?.index?.hidden === 'true', + data_stream: indexData.data_stream, + }; }); - - // System indices may show up in _cat APIs, as these APIs are primarily used for troubleshooting - // For now, we filter them out and only return index information for the indices we have - // In the future, we should migrate away from using cat APIs (https://github.com/elastic/kibana/issues/57286) - return catHits.reduce((decoratedIndices, hit) => { - const index = indices[hit.index!]; - - if (typeof index !== 'undefined') { - const aliases = Object.keys(index.aliases!); - - decoratedIndices.push({ - health: hit.health!, - status: hit.status!, - name: hit.index!, - uuid: hit.uuid!, - primary: hit.pri!, - replica: hit.rep!, - documents: hit['docs.count'], - size: hit['store.size'], - isFrozen: hit.sth === 'true', // sth value coming back as a string from ES - aliases: aliases.length ? aliases : 'none', - // @ts-expect-error @elastic/elasticsearch https://github.com/elastic/elasticsearch-specification/issues/532 - hidden: index.settings?.index.hidden === 'true', - data_stream: index.data_stream!, - }); - } - - return decoratedIndices; - }, [] as Index[]); } export const fetchIndices = async ( diff --git a/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts b/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts new file mode 100644 index 0000000000000..51793bf4d6347 --- /dev/null +++ b/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts @@ -0,0 +1,47 @@ +/* + * 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 { IndicesIndexState, IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types'; +import { Index } from '../..'; + +// fixtures return minimal index properties needed for API tests + +export const createTestIndexState = (index?: Partial) => { + return { + aliases: {}, + settings: { index: { number_of_shards: 1, number_of_replicas: 1 } }, + ...index, + }; +}; + +export const createTestIndexStats = (index?: Partial) => { + return { + health: 'green', + status: 'open', + uuid: 'test_index', + total: { docs: { count: 1 }, store: { size_in_bytes: 100 } }, + ...index, + }; +}; + +export const createTestIndexResponse = (index?: Partial) => { + return { + aliases: 'none', + data_stream: undefined, + documents: 1, + health: 'green', + hidden: false, + isFrozen: false, + name: 'test_index', + primary: 1, + replica: 1, + size: '100b', + status: 'open', + uuid: 'test_index', + ...index, + }; +}; diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index 7cb6950207f9b..3a6bc03e29f59 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -217,11 +217,12 @@ export default function ({ getService }) { }); describe('reload', function () { - // FLAKY: https://github.com/elastic/kibana/issues/90565 - describe.skip('(not on Cloud)', function () { + describe('(not on Cloud)', function () { this.tags(['skipCloud']); it('should list all the indices with the expected properties and data enrichers', async function () { + // create an index to assert against, otherwise the test is flaky + await createIndex('reload-test-index'); const { body } = await reload().expect(200); const expectedKeys = [ 'health', @@ -243,7 +244,9 @@ export default function ({ getService }) { // We need to sort the keys before comparing then, because race conditions // can cause enrichers to register in non-deterministic order. const sortedExpectedKeys = expectedKeys.sort(); - const sortedReceivedKeys = Object.keys(body[0]).sort(); + + const indexCreated = body.find((index) => index.name === 'reload-test-index'); + const sortedReceivedKeys = Object.keys(indexCreated).sort(); expect(sortedReceivedKeys).to.eql(sortedExpectedKeys); expect(body.length > 1).to.be(true); // to contrast it with the next test });