diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 4a3ba58b8..302537799 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -373,3 +373,10 @@ export const ALIAS_SELECT_RULE = [ }, }, ]; + +export enum IndexOpBlocksType { + Closed = "4", + ReadOnly = "5", + MetaData = "9", + ReadOnlyAllowDelete = "12", +} diff --git a/public/utils/helpers.test.ts b/public/utils/helpers.test.ts index 41e55e538..a5c1d4a7c 100644 --- a/public/utils/helpers.test.ts +++ b/public/utils/helpers.test.ts @@ -2,7 +2,46 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { diffJson } from "./helpers"; +import { + diffJson, + getBlockedIndices, + indexBlockedPredicate, + aliasBlockedPredicate, + dataStreamBlockedPredicate, + filterBlockedItems, +} from "./helpers"; +import { CatIndex, DataStream } from "../../server/models/interfaces"; +import { IAlias } from "../pages/Aliases/interface"; +import { browserServicesMock } from "../../test/mocks"; +import { IndexOpBlocksType } from "./constants"; + +const exampleBlocksStateResponse = { + cluster_name: "opensearch-cluster", + cluster_uuid: "123", + blocks: { + indices: { + test_index1: { + "4": { + description: "index closed", + retryable: false, + levels: ["read", "write"], + }, + "5": { + description: "index read-only (api)", + retryable: false, + levels: ["write", "metadata_write"], + }, + }, + test_index2: { + "4": { + description: "index closed", + retryable: false, + levels: ["read", "write"], + }, + }, + }, + }, +}; describe("helpers spec", () => { it(`diffJson`, async () => { @@ -38,4 +77,162 @@ describe("helpers spec", () => { ) ).toEqual(2); }); + + it(`getBlockedIndices normal case`, async () => { + browserServicesMock.commonService.apiCaller = jest.fn().mockResolvedValue({ + ok: true, + response: exampleBlocksStateResponse, + }); + expect(getBlockedIndices(browserServicesMock)).resolves.toEqual({ + test_index1: ["4", "5"], + test_index2: ["4"], + }); + }); + + it(`getBlockedIndices empty case`, async () => { + browserServicesMock.commonService.apiCaller = jest.fn().mockResolvedValue({ + ok: true, + response: { + cluster_name: "opensearch-cluster", + cluster_uuid: "123", + blocks: {}, + }, + }); + expect(getBlockedIndices(browserServicesMock)).resolves.toEqual({}); + }); + + it(`getBlockedIndices error case`, async () => { + browserServicesMock.commonService.apiCaller = jest.fn().mockResolvedValue({ + ok: false, + error: "test", + }); + try { + await getBlockedIndices(browserServicesMock); + throw "fail"; + } catch (err) { + expect(err).toEqual("test"); + } + }); + + it(`indexBlockedPredicate`, async () => { + const blockedItemsSet = new Set(["index_1", "index_2"]); + expect(indexBlockedPredicate({ index: "index_1" }, blockedItemsSet)).toEqual(true); + expect(indexBlockedPredicate({ index: "index_3" }, blockedItemsSet)).toEqual(false); + }); + + it(`aliasBlockedPredicate`, async () => { + const blockedItemsSet = new Set(["index_1", "index_2"]); + expect(aliasBlockedPredicate({ indexArray: ["index_1", "index_3"] }, blockedItemsSet)).toEqual(true); + expect(aliasBlockedPredicate({ indexArray: ["index_3", "index_4"] }, blockedItemsSet)).toEqual(false); + }); + + it(`dataStreamBlockedPredicate`, async () => { + const blockedItemsSet = new Set(["index_1", "index_2"]); + expect(dataStreamBlockedPredicate({ indices: [{ index_name: "index_1" }, { index_name: "index_3" }] }, blockedItemsSet)).toEqual(true); + expect(dataStreamBlockedPredicate({ indices: [{ index_name: "index_4" }, { index_name: "index_3" }] }, blockedItemsSet)).toEqual(false); + }); + + it(`filterBlockedItems index`, async () => { + browserServicesMock.commonService.apiCaller = jest.fn().mockResolvedValue({ + ok: true, + response: exampleBlocksStateResponse, + }); + const selectedItems = [{ index: "test_index1" }, { index: "test_index2" }, { index: "test_index3" }]; + expect( + filterBlockedItems(browserServicesMock, selectedItems, IndexOpBlocksType.Closed, indexBlockedPredicate) + ).resolves.toEqual({ + blockedItems: [{ index: "test_index1" }, { index: "test_index2" }], + unBlockedItems: [{ index: "test_index3" }], + }); + + expect( + filterBlockedItems( + browserServicesMock, + selectedItems, + [IndexOpBlocksType.ReadOnly, IndexOpBlocksType.MetaData], + indexBlockedPredicate + ) + ).resolves.toEqual({ + blockedItems: [{ index: "test_index1" }], + unBlockedItems: [{ index: "test_index2" }, { index: "test_index3" }], + }); + + browserServicesMock.commonService.apiCaller = jest.fn().mockResolvedValue({ + ok: true, + response: { blocks: {} }, + }); + + expect( + filterBlockedItems( + browserServicesMock, + selectedItems, + [IndexOpBlocksType.ReadOnly, IndexOpBlocksType.MetaData], + indexBlockedPredicate + ) + ).resolves.toEqual({ + blockedItems: [], + unBlockedItems: selectedItems, + }); + }); + + it(`filterBlockedItems alias`, async () => { + browserServicesMock.commonService.apiCaller = jest.fn().mockResolvedValue({ + ok: true, + response: exampleBlocksStateResponse, + }); + const selectedItems = [ + { indexArray: ["test_index1", "test_index3"] }, + { indexArray: ["test_index1", "test_index3"] }, + { indexArray: ["test_index2", "test_index3"] }, + ]; + + expect( + filterBlockedItems(browserServicesMock, selectedItems, IndexOpBlocksType.Closed, aliasBlockedPredicate) + ).resolves.toEqual({ + blockedItems: selectedItems, + unBlockedItems: [], + }); + + expect( + filterBlockedItems( + browserServicesMock, + selectedItems, + [IndexOpBlocksType.ReadOnly, IndexOpBlocksType.MetaData], + aliasBlockedPredicate + ) + ).resolves.toEqual({ + blockedItems: [selectedItems[0], selectedItems[1]], + unBlockedItems: [selectedItems[2]], + }); + }); + + it(`filterBlockedItems dataStream`, async () => { + browserServicesMock.commonService.apiCaller = jest.fn().mockResolvedValue({ + ok: true, + response: exampleBlocksStateResponse, + }); + const selectedItems = [ + { indices: [{ index_name: "test_index1" }, { index_name: "test_index2" }] }, + { indices: [{ index_name: "test_index1" }, { index_name: "test_index3" }] }, + { indices: [{ index_name: "test_index2" }, { index_name: "test_index3" }] }, + ]; + expect( + filterBlockedItems(browserServicesMock, selectedItems, IndexOpBlocksType.Closed, dataStreamBlockedPredicate) + ).resolves.toEqual({ + blockedItems: selectedItems, + unBlockedItems: [], + }); + + expect( + filterBlockedItems( + browserServicesMock, + selectedItems, + [IndexOpBlocksType.ReadOnly, IndexOpBlocksType.MetaData], + dataStreamBlockedPredicate + ) + ).resolves.toEqual({ + blockedItems: [selectedItems[0], selectedItems[1]], + unBlockedItems: [selectedItems[2]], + }); + }); }); diff --git a/public/utils/helpers.ts b/public/utils/helpers.ts index 07dd7ee6f..585c88a3c 100644 --- a/public/utils/helpers.ts +++ b/public/utils/helpers.ts @@ -6,6 +6,10 @@ // @ts-ignore import { htmlIdGenerator } from "@elastic/eui/lib/services"; import { isEqual } from "lodash"; +import { BrowserServices } from "../models/interfaces"; +import { IndexOpBlocksType } from "./constants"; +import { CatIndex, DataStream } from "../../server/models/interfaces"; +import { IAlias } from "../pages/Aliases/interface"; export function getErrorMessage(err: any, defaultMessage: string) { if (err && err.message) return err.message; @@ -45,3 +49,102 @@ export function diffJson(oldJson?: Record, newJson?: Record(services, selectedItems, IndexOpBlocksType.Closed, aliasBlockedPredicate) + +interface BlockedIndices { + [indexName: string]: String[]; +} + +interface ClusterBlocksStateResponse { + blocks: { + indices?: { + [indexName: string]: { + [blockId: string]: { + description: string; + retryable: boolean; + levels: string[]; + }; + }; + }; + }; +} + +export async function getBlockedIndices(broswerServices: BrowserServices): Promise { + const result = await broswerServices.commonService.apiCaller({ + endpoint: "cluster.state", + data: { + metric: "blocks", + }, + }); + if (!result.ok) { + throw result.error; + } + + const blocksResponse = result.response.blocks; + if (!blocksResponse.indices) { + return {}; + } + + const blockedIndices: BlockedIndices = {}; + Object.keys(blocksResponse.indices).forEach((indexName) => { + const innerBlocksInfo = blocksResponse.indices![indexName]; + const indexOpBlocksIds = Object.keys(innerBlocksInfo); + blockedIndices[indexName] = indexOpBlocksIds; + }); + return blockedIndices; +} + +export interface FilteredBlockedItems { + unBlockedItems: T[]; + blockedItems: T[]; +} + +export function indexBlockedPredicate(item: Pick, blockedItemsSet: Set): boolean { + return blockedItemsSet.has(item.index); +} + +export function aliasBlockedPredicate(item: Pick, blockedItemsSet: Set): boolean { + return !!item.indexArray.find((indexName) => blockedItemsSet.has(indexName)); +} + +export function dataStreamBlockedPredicate(item: Pick, blockedItemsSet: Set): boolean { + return !!item.indices.find((dataStreamIndex) => blockedItemsSet.has(dataStreamIndex.index_name)); +} + +export async function filterBlockedItems( + broswerServices: BrowserServices, + inputItems: T[], + blocksTypes: IndexOpBlocksType | IndexOpBlocksType[], + blocksPredicate: (item: T, blockedItemsSet: Set) => boolean +): Promise> { + const blocksTypesSet = new Set(Array.isArray(blocksTypes) ? blocksTypes : [blocksTypes]); + const blockedIndices = await getBlockedIndices(broswerServices); + // we only care about the indices with blocks type in blocksTypesSet + // use set to accelarate execution + const filteredBlockedIndicesSet = new Set( + Object.entries(blockedIndices) + .filter( + // blockedIndex is like this: ["index_name": ["4","5"]] + (blockedIndex) => blockedIndex[1].find((s) => blocksTypesSet.has(s as IndexOpBlocksType)) + // we only take index name, do not need blocksType + ) + .map((blockedIndex) => blockedIndex[0]) + ); + const result: FilteredBlockedItems = { + unBlockedItems: [], + blockedItems: [], + }; + inputItems.forEach((item) => { + if (blocksPredicate(item, filteredBlockedIndicesSet)) { + result.blockedItems.push(item); + } else { + result.unBlockedItems.push(item); + } + }); + return result; +}