diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 30ce2fdd79106..fe2c3378b59b2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -883,7 +883,7 @@ x-pack/platform/plugins/private/snapshot_restore @elastic/kibana-management x-pack/platform/plugins/private/telemetry_collection_xpack @elastic/kibana-core x-pack/platform/plugins/private/transform @elastic/ml-ui x-pack/platform/plugins/private/translations @elastic/kibana-localization -x-pack/platform/plugins/private/upgrade_assistant @elastic/kibana-core +x-pack/platform/plugins/private/upgrade_assistant @elastic/kibana-management x-pack/platform/plugins/private/watcher @elastic/kibana-management x-pack/platform/plugins/shared/actions @elastic/response-ops x-pack/platform/plugins/shared/ai_infra/llm_tasks @elastic/appex-ai-infra diff --git a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts index d65fb911ae456..0f7a7edd2867a 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts @@ -200,6 +200,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D trainedModels: `${MACHINE_LEARNING_DOCS}ml-trained-models.html`, textEmbedding: `${MACHINE_LEARNING_DOCS}ml-nlp-model-ref.html#ml-nlp-model-ref-text-embedding`, troubleshootSetup: `${ENTERPRISE_SEARCH_DOCS}troubleshoot-setup.html`, + upgrade9x: `${ENTERPRISE_SEARCH_DOCS}upgrading-to-9-x.html`, usersAccess: `${ENTERPRISE_SEARCH_DOCS}users-access.html`, }, metricbeat: { @@ -301,6 +302,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D upgradeAssistant: { overview: `${KIBANA_DOCS}upgrade-assistant.html`, batchReindex: `${KIBANA_DOCS}batch-start-resume-reindex.html`, + indexBlocks: `${ELASTICSEARCH_DOCS}index-modules-blocks.html#index-block-settings`, remoteReindex: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-from-remote`, unfreezeApi: `${ELASTICSEARCH_DOCS}unfreeze-index-api.html`, reindexWithPipeline: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-with-an-ingest-pipeline`, diff --git a/src/platform/packages/shared/kbn-doc-links/src/types.ts b/src/platform/packages/shared/kbn-doc-links/src/types.ts index d1f7f3a2a7930..ed2f4a598e806 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/types.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/types.ts @@ -169,6 +169,7 @@ export interface DocLinks { readonly textEmbedding: string; readonly troubleshootSetup: string; readonly usersAccess: string; + readonly upgrade9x: string; }; readonly heartbeat: { readonly base: string; @@ -261,6 +262,7 @@ export interface DocLinks { readonly upgradeAssistant: { readonly overview: string; readonly batchReindex: string; + readonly indexBlocks: string; readonly remoteReindex: string; readonly unfreezeApi: string; readonly reindexWithPipeline: string; diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 9686ceb283e22..217a84d898bcd 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -47450,9 +47450,6 @@ "xpack.upgradeAssistant.esDeprecations.clusterSettings.deletingButtonLabel": "Retrait des paramètres en cours…", "xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionText": "Retirer les paramètres", "xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionTooltipLabel": "Corrigez ce problème en retirant les paramètres de ce cluster. La correction peut s'effectuer automatiquement.", - "xpack.upgradeAssistant.esDeprecations.dataStream.reindexLoadingStatusText": "Chargement du statut…", - "xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLabel": "Marquer en lecture seule ou réindexer", - "xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipLabel": "Ce problème peut être résolu en réindexant ce flux de données ou en marquant ses index en lecture seule. La correction peut s'effectuer automatiquement.", "xpack.upgradeAssistant.esDeprecations.dataStreamsTypeLabel": "Flux de données", "xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellLabel": "Manuel", "xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel": "Fermer", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index ed45cbd99405e..4113de47d1eb5 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -47415,9 +47415,6 @@ "xpack.upgradeAssistant.esDeprecations.clusterSettings.deletingButtonLabel": "設定を削除中…", "xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionText": "設定の削除", "xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionTooltipLabel": "このクラスターから設定を削除して、この問題を解決します。この問題は自動的に解決できます。", - "xpack.upgradeAssistant.esDeprecations.dataStream.reindexLoadingStatusText": "ステータスを読み込んでいます...", - "xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLabel": "読み取り専用に設定または再インデックス", - "xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipLabel": "この問題を解決するには、このデータストリームを再インデックス化するか、そのインデックスを読み取り専用に設定します。この問題は自動的に解決できます。", "xpack.upgradeAssistant.esDeprecations.dataStreamsTypeLabel": "データストリーム", "xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellLabel": "手動", "xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel": "閉じる", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 7e2497d734013..a72d135635b1f 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -47489,9 +47489,6 @@ "xpack.upgradeAssistant.esDeprecations.clusterSettings.deletingButtonLabel": "设置移除进行中……", "xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionText": "移除设置", "xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionTooltipLabel": "通过从此集群中移除设置来解决该问题。此问题会自动解决。", - "xpack.upgradeAssistant.esDeprecations.dataStream.reindexLoadingStatusText": "正在加载状态……", - "xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLabel": "标记为只读,或重新索引", - "xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipLabel": "请通过重新索引该数据流或将其索引标记为只读来解决此问题。此问题会自动解决。", "xpack.upgradeAssistant.esDeprecations.dataStreamsTypeLabel": "数据流", "xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellLabel": "手动", "xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel": "关闭", diff --git a/x-pack/platform/plugins/private/upgrade_assistant/README.md b/x-pack/platform/plugins/private/upgrade_assistant/README.md index 0272a958659fc..e7d0d2ab9b00d 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/README.md +++ b/x-pack/platform/plugins/private/upgrade_assistant/README.md @@ -29,7 +29,7 @@ These surface runtime deprecations, e.g. a Painless script that uses a deprecate request to a deprecated API. These are also generally surfaced as deprecation headers within the response. Even if the cluster state is good, app maintainers need to watch the logs in case deprecations are discovered as data is migrated. Starting in 7.x, deprecation logs can be written to a file or a data stream ([#58924](https://github.com/elastic/elasticsearch/pull/58924)). When the data stream exists, the Upgrade Assistant provides a way to analyze the logs through Observability or Discover ([#106521](https://github.com/elastic/kibana/pull/106521)). -* [**Kibana deprecations API.**](https://github.com/elastic/kibana/blob/main/src/core/server/docs/kib_core_deprecations_service.mdx) This is information about deprecated features and configs in Kibana. These deprecations are only communicated to the user if the deployment is using these features. Kibana engineers are responsible for adding deprecations to the deprecations API for their respective team. +* [**Kibana deprecations API.**](https://github.com/elastic/kibana/blob/main/src/core/server/deprecations/README.mdx) This is information about deprecated features and configs in Kibana. These deprecations are only communicated to the user if the deployment is using these features. Kibana engineers are responsible for adding deprecations to the deprecations API for their respective team. ### Fixing problems diff --git a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/es_deprecations/mocked_responses.ts b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/es_deprecations/mocked_responses.ts index 2dc33c29de9ec..8992c79a95c15 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/es_deprecations/mocked_responses.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/es_deprecations/mocked_responses.ts @@ -35,6 +35,11 @@ export const MOCK_REINDEX_DEPRECATION: EnrichedDeprecationInfo = { index: 'reindex_index', correctiveAction: { type: 'reindex', + metadata: { + isClosedIndex: false, + isFrozenIndex: false, + isInDataStream: false, + }, }, }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts index ad5d5d74f8059..805a564b2ad59 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts @@ -21,6 +21,9 @@ const defaultReindexStatusMeta: ReindexStatusResponse['meta'] = { indexName: 'foo', reindexName: 'reindexed-foo', aliases: [], + isFrozen: false, + isReadonly: false, + isInDataStream: false, }; describe('Reindex deprecation flyout', () => { @@ -75,28 +78,22 @@ describe('Reindex deprecation flyout', () => { expect(exists('reindexDetails')).toBe(true); expect(find('reindexDetails.flyoutTitle').text()).toContain( - `Reindex ${reindexDeprecation.index}` + `Update ${reindexDeprecation.index}` ); }); it('renders error callout when reindex fails', async () => { - await act(async () => { - testBed = await setupElasticsearchPage(httpSetup); - }); - - testBed.component.update(); - - const { actions, exists } = testBed; - - await actions.table.clickDeprecationRowAt('reindex', 0); - httpRequestsMockHelpers.setStartReindexingResponse(MOCK_REINDEX_DEPRECATION.index!, undefined, { statusCode: 404, message: 'no such index [test]', }); - await actions.reindexDeprecationFlyout.clickReindexButton(); + const { actions, exists } = testBed; + await actions.table.clickDeprecationRowAt('reindex', 0); + await actions.reindexDeprecationFlyout.clickReindexButton(); // details step + await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step + expect(exists('reindexDetails')).toBe(true); expect(exists('reindexDetails.reindexingFailedCallout')).toBe(true); }); @@ -106,28 +103,16 @@ describe('Reindex deprecation flyout', () => { message: 'no such index [test]', }); - await act(async () => { - testBed = await setupElasticsearchPage(httpSetup); - }); - - testBed.component.update(); - const { actions, exists } = testBed; await actions.table.clickDeprecationRowAt('reindex', 0); + await actions.reindexDeprecationFlyout.clickReindexButton(); // details step + await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step expect(exists('reindexDetails.fetchFailedCallout')).toBe(true); }); describe('reindexing progress', () => { - it('has not started yet', async () => { - const { actions, find, exists } = testBed; - - await actions.table.clickDeprecationRowAt('reindex', 0); - expect(find('reindexChecklistTitle').text()).toEqual('Reindexing process'); - expect(exists('cancelReindexingDocumentsButton')).toBe(false); - }); - it('has started but not yet reindexing documents', async () => { httpRequestsMockHelpers.setReindexStatusResponse(MOCK_REINDEX_DEPRECATION.index!, { reindexOp: { @@ -140,14 +125,11 @@ describe('Reindex deprecation flyout', () => { meta: defaultReindexStatusMeta, }); - await act(async () => { - testBed = await setupElasticsearchPage(httpSetup); - }); - - testBed.component.update(); const { actions, find, exists } = testBed; await actions.table.clickDeprecationRowAt('reindex', 0); + await actions.reindexDeprecationFlyout.clickReindexButton(); // details step + await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 5%'); expect(exists('cancelReindexingDocumentsButton')).toBe(false); @@ -165,14 +147,11 @@ describe('Reindex deprecation flyout', () => { meta: defaultReindexStatusMeta, }); - await act(async () => { - testBed = await setupElasticsearchPage(httpSetup); - }); - - testBed.component.update(); const { actions, find, exists } = testBed; await actions.table.clickDeprecationRowAt('reindex', 0); + await actions.reindexDeprecationFlyout.clickReindexButton(); // details step + await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 30%'); expect(exists('cancelReindexingDocumentsButton')).toBe(true); @@ -190,14 +169,11 @@ describe('Reindex deprecation flyout', () => { meta: defaultReindexStatusMeta, }); - await act(async () => { - testBed = await setupElasticsearchPage(httpSetup); - }); - - testBed.component.update(); const { actions, find, exists } = testBed; await actions.table.clickDeprecationRowAt('reindex', 0); + await actions.reindexDeprecationFlyout.clickReindexButton(); // details step + await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 90%'); expect(exists('cancelReindexingDocumentsButton')).toBe(false); @@ -215,14 +191,11 @@ describe('Reindex deprecation flyout', () => { meta: defaultReindexStatusMeta, }); - await act(async () => { - testBed = await setupElasticsearchPage(httpSetup); - }); - const { actions, find, exists, component } = testBed; - component.update(); await actions.table.clickDeprecationRowAt('reindex', 0); + await actions.reindexDeprecationFlyout.clickReindexButton(); // details step + await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 95%'); expect(exists('cancelReindexingDocumentsButton')).toBe(false); @@ -250,14 +223,11 @@ describe('Reindex deprecation flyout', () => { }, ]); - await act(async () => { - testBed = await setupElasticsearchPage(httpSetup); - }); - - testBed.component.update(); const { actions, find } = testBed; await actions.table.clickDeprecationRowAt('reindex', 0); + await actions.reindexDeprecationFlyout.clickReindexButton(); // details step + await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step expect(find('lowDiskSpaceCallout').text()).toContain('Nodes with low disk space'); expect(find('impactedNodeListItem').length).toEqual(1); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts index ada2a110541f3..6beb3e8ba99e3 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts @@ -51,6 +51,7 @@ const servicesMock = { const idToUrlMap = { SNAPSHOT_RESTORE_LOCATOR: 'snapshotAndRestoreUrl', DISCOVER_APP_LOCATOR: 'discoverUrl', + OBS_LOGS_EXPLORER_DATA_VIEW_LOCATOR: 'logsExplorerUrl', }; type IdKey = keyof typeof idToUrlMap; @@ -75,6 +76,7 @@ shareMock.url.locators.get = (id: IdKey) => ({ }); export const getAppContextMock = (kibanaVersion: SemVer) => ({ + dataSourceExclusions: {}, featureSet: { mlSnapshots: true, migrateSystemIndices: true, diff --git a/x-pack/platform/plugins/private/upgrade_assistant/common/data_stream_types.ts b/x-pack/platform/plugins/private/upgrade_assistant/common/data_stream_types.ts index 3ff6dbfb0535e..e15b05f86ffda 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/common/data_stream_types.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/common/data_stream_types.ts @@ -6,6 +6,7 @@ */ export interface DataStreamsActionMetadata { + excludedActions?: Array<'readOnly' | 'reindex'>; totalBackingIndices: number; indicesRequiringUpgradeCount: number; indicesRequiringUpgrade: string[]; @@ -14,6 +15,7 @@ export interface DataStreamsActionMetadata { reindexRequired: boolean; } +export type DataStreamResolutionType = 'readonly' | 'reindex'; export interface DataStreamsAction { type: 'dataStream'; metadata: DataStreamsActionMetadata; @@ -34,21 +36,22 @@ export interface DataStreamMetadata { } export interface DataStreamReindexStatusResponse { - warnings?: DataStreamReindexWarning[]; - reindexOp?: DataStreamReindexOperation; + warnings?: DataStreamMigrationWarning[]; + migrationOp?: DataStreamMigrationOperation; hasRequiredPrivileges?: boolean; } -export type DataStreamReindexWarningTypes = 'incompatibleDataStream'; +export type DataStreamWarningTypes = 'incompatibleDataStream' | 'affectExistingSetups'; -export interface DataStreamReindexWarning { - warningType: DataStreamReindexWarningTypes; +export interface DataStreamMigrationWarning { + warningType: DataStreamWarningTypes; + resolutionType: DataStreamResolutionType; meta?: { [key: string]: string | string[]; }; } -export enum DataStreamReindexStatus { +export enum DataStreamMigrationStatus { notStarted, inProgress, completed, @@ -66,31 +69,35 @@ export interface DataStreamProgressDetails { } export interface DataStreamReindexStatusNotStarted { - status: DataStreamReindexStatus.notStarted; + status: DataStreamMigrationStatus.notStarted; } export interface DataStreamReindexStatusInProgress { - status: DataStreamReindexStatus.inProgress; - reindexTaskPercComplete: number; + resolutionType: 'reindex' | 'readonly'; + status: DataStreamMigrationStatus.inProgress; + taskPercComplete: number; progressDetails: DataStreamProgressDetails; } export interface DataStreamReindexStatusCompleted { - status: DataStreamReindexStatus.completed; - reindexTaskPercComplete: number; + resolutionType: 'reindex' | 'readonly'; + status: DataStreamMigrationStatus.completed; + taskPercComplete: number; progressDetails: DataStreamProgressDetails; } export interface DataStreamReindexStatusFailed { - status: DataStreamReindexStatus.failed; + resolutionType: 'reindex' | 'readonly'; + status: DataStreamMigrationStatus.failed; errorMessage: string; } export interface DataStreamReindexStatusCancelled { - status: DataStreamReindexStatus.cancelled; + resolutionType: 'reindex' | 'readonly'; + status: DataStreamMigrationStatus.cancelled; } -export type DataStreamReindexOperation = +export type DataStreamMigrationOperation = | DataStreamReindexStatusNotStarted | DataStreamReindexStatusInProgress | DataStreamReindexStatusCompleted diff --git a/x-pack/platform/plugins/private/upgrade_assistant/common/types.ts b/x-pack/platform/plugins/private/upgrade_assistant/common/types.ts index bfcf54baec771..62ea8cc8376a9 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/common/types.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/common/types.ts @@ -6,7 +6,7 @@ */ import { HealthReportImpact } from '@elastic/elasticsearch/lib/api/types'; -import type { estypes } from '@elastic/elasticsearch'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { SavedObject } from '@kbn/core/types'; import type { DataStreamsAction } from './data_stream_types'; @@ -54,8 +54,11 @@ export interface ReindexStatusResponse { reindexName: string; // Array of aliases pointing to the index being reindexed aliases: string[]; + isReadonly: boolean; + isFrozen: boolean; + isInDataStream: boolean; }; - warnings?: ReindexWarning[]; + warnings?: IndexWarning[]; reindexOp?: ReindexOperation; hasRequiredPrivileges?: boolean; } @@ -136,10 +139,11 @@ export interface ReindexOperation { export type ReindexSavedObject = SavedObject; // 8.0 -> 9.0 warnings -export type ReindexWarningTypes = 'indexSetting' | 'replaceIndexWithAlias'; +export type IndexWarningType = 'indexSetting' | 'replaceIndexWithAlias' | 'makeIndexReadonly'; -export interface ReindexWarning { - warningType: ReindexWarningTypes; +export interface IndexWarning { + warningType: IndexWarningType; + flow: 'reindex' | 'readonly' | 'all'; /** * Optional metadata for deprecations * @@ -147,7 +151,7 @@ export interface ReindexWarning { * For "indexSetting" we want to surface the deprecated settings. */ meta?: { - [key: string]: string | string[]; + [key: string]: string | string[] | boolean; }; } @@ -190,16 +194,36 @@ export interface DeprecationInfo { export interface IndexSettingsDeprecationInfo { [indexName: string]: DeprecationInfo[]; } -export interface ReindexAction { + +export interface IndexMetadata { + isFrozenIndex: boolean; + isInDataStream: boolean; + isClosedIndex: boolean; +} + +export interface IndexAction { + /** + * Includes relevant information about the index related to this action + */ + metadata: IndexMetadata; +} + +export interface ReindexAction extends IndexAction { type: 'reindex'; + /** - * Indicate what blockers have been detected for calling reindex - * against this index. - * - * @remark - * In future this could be an array of blockers. + * The transform IDs that are currently targeting this index + */ + transformIds?: string[]; + + /** + * The actions that should be excluded from the reindex corrective action. */ - blockerForReindexing?: 'index-closed'; // 'index-closed' can be handled automatically, but requires more resources, user should be warned + excludedActions?: string[]; +} + +export interface UnfreezeAction extends IndexAction { + type: 'unfreeze'; } export interface MlAction { @@ -225,6 +249,15 @@ export interface HealthIndicatorAction { impacts: HealthReportImpact[]; } +export type CorrectiveAction = + | ReindexAction + | UnfreezeAction + | MlAction + | IndexSettingAction + | ClusterSettingAction + | DataStreamsAction + | HealthIndicatorAction; + export interface EnrichedDeprecationInfo extends Omit< estypes.MigrationDeprecationsDeprecation, @@ -237,16 +270,9 @@ export interface EnrichedDeprecationInfo | 'ilm_policies' | 'templates'; isCritical: boolean; - frozen?: boolean; status?: estypes.HealthReportIndicatorHealthStatus; index?: string; - correctiveAction?: - | ReindexAction - | MlAction - | IndexSettingAction - | ClusterSettingAction - | DataStreamsAction - | HealthIndicatorAction; + correctiveAction?: CorrectiveAction; resolveDuringUpgrade: boolean; } @@ -322,3 +348,5 @@ export interface FeatureSet { reindexCorrectiveActions: boolean; migrateDataStreams: boolean; } + +export type DataSourceExclusions = Record>; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/common/update_index.ts b/x-pack/platform/plugins/private/upgrade_assistant/common/update_index.ts new file mode 100644 index 0000000000000..e9421e6807d58 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/common/update_index.ts @@ -0,0 +1,8 @@ +/* + * 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 type UpdateIndexOperation = 'blockWrite' | 'unfreeze'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/kibana.jsonc b/x-pack/platform/plugins/private/upgrade_assistant/kibana.jsonc index 24b328c1294bc..55a08297937bb 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/kibana.jsonc +++ b/x-pack/platform/plugins/private/upgrade_assistant/kibana.jsonc @@ -1,9 +1,7 @@ { "type": "plugin", "id": "@kbn/upgrade-assistant-plugin", - "owner": [ - "@elastic/kibana-core" - ], + "owner": "@elastic/kibana-management", "group": "platform", "visibility": "private", "plugin": { diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/common/_step_progress.scss similarity index 55% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/common/_step_progress.scss index 6d86f652e7252..faed0a83f1244 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/common/_step_progress.scss @@ -11,10 +11,25 @@ margin-right: $euiSizeM; } +$stepStatusToCallOutColor: ( + failed: $euiColorDanger, + complete: $euiColorSuccess, + paused: $euiColorWarning, + cancelled: $euiColorWarning, +); + .upgStepProgress__status--circle { text-align: center; border-radius: $euiSizeM; line-height: $euiSize - 2px; + + @each $status, $callOutColor in $stepStatusToCallOutColor { + &-#{$status} { + $statusBg: tintOrShade($callOutColor, 90%, 70%); + color: shadeOrTint(makeHighContrastColor($callOutColor, $statusBg), 0, 20%); + background-color: $statusBg; + } + } } .upgStepProgress__title { diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/common/step_progress.tsx similarity index 68% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/common/step_progress.tsx index 9d272df771d2d..a6e4f80223941 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/common/step_progress.tsx @@ -8,64 +8,38 @@ import classNames from 'classnames'; import React, { Fragment, ReactNode } from 'react'; -import { EuiIcon, EuiLoadingSpinner, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; +import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import './_step_progress.scss'; type STATUS = 'incomplete' | 'inProgress' | 'complete' | 'failed' | 'paused' | 'cancelled'; const StepStatus: React.FunctionComponent<{ status: STATUS; idx: number }> = ({ status, idx }) => { - const { euiTheme } = useEuiTheme(); if (status === 'incomplete') { return {idx + 1}.; } else if (status === 'inProgress') { return ; } else if (status === 'complete') { return ( - + ); } else if (status === 'paused') { return ( - + ); } else if (status === 'cancelled') { return ( - + ); } else if (status === 'failed') { return ( - + ); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/context.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/context.tsx index 2744caa51edf5..90994f044693f 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/context.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/context.tsx @@ -8,22 +8,31 @@ import React, { createContext, useContext } from 'react'; import { ApiService } from '../../../../lib/api'; -import { useReindexStatus, ReindexState } from './use_reindex_state'; +import { useMigrationStatus, MigrationState } from './use_migration_state'; +import type { DataStreamResolutionType } from '../../../../../../common/types'; -export interface ReindexStateContext { - reindexState: ReindexState; - startReindex: () => Promise; +export interface MigrationStateContext { loadDataStreamMetadata: () => Promise; + migrationState: MigrationState; + + initMigration: (resolutionType: DataStreamResolutionType) => void; + + // reindex resolution actions + startReindex: () => Promise; cancelReindex: () => Promise; + + // readonly resolution actions + startReadonly: () => Promise; + cancelReadonly: () => Promise; } -const DataStreamReindexContext = createContext(undefined); +const DataStreamMigrationContext = createContext(undefined); -export const useDataStreamReindexContext = () => { - const context = useContext(DataStreamReindexContext); +export const useDataStreamMigrationContext = () => { + const context = useContext(DataStreamMigrationContext); if (context === undefined) { throw new Error( - 'useDataStreamReindexContext must be used within a ' + 'useDataStreamMigrationContext must be used within a ' ); } return context; @@ -35,26 +44,37 @@ interface Props { dataStreamName: string; } -export const DataStreamReindexStatusProvider: React.FunctionComponent = ({ +export const DataStreamMigrationStatusProvider: React.FunctionComponent = ({ api, dataStreamName, children, }) => { - const { reindexState, startReindex, loadDataStreamMetadata, cancelReindex } = useReindexStatus({ + const { + migrationState, + cancelReadonly, + startReindex, + loadDataStreamMetadata, + cancelReindex, + startReadonly, + initMigration, + } = useMigrationStatus({ dataStreamName, api, }); return ( - {children} - + ); }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/container.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/container.tsx index 26c0c2f492404..c518bbe74a92f 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/container.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/container.tsx @@ -18,29 +18,33 @@ import { METRIC_TYPE } from '@kbn/analytics'; import moment from 'moment'; import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; import { - DataStreamReindexStatus, + DataStreamMigrationStatus, + DataStreamsAction, EnrichedDeprecationInfo, } from '../../../../../../../common/types'; -import { ReindexStateContext } from '../context'; +import { MigrationStateContext } from '../context'; import { DeprecationBadge } from '../../../../shared'; import { UIM_DATA_STREAM_REINDEX_START_CLICK, UIM_DATA_STREAM_REINDEX_STOP_CLICK, + UIM_DATA_STREAM_START_READONLY_CLICK, + UIM_DATA_STREAM_STOP_READONLY_CLICK, uiMetricService, } from '../../../../../lib/ui_metric'; import { containerMessages } from './messages'; import type { FlyoutStep } from './steps/types'; import { InitializingFlyoutStep } from './steps/initializing'; -import { ConfirmReindexingFlyoutStep } from './steps/confirm'; +import { ConfirmMigrationFlyoutStep } from './steps/confirm'; import { DataStreamDetailsFlyoutStep } from './steps/details'; import { ChecklistFlyoutStep } from './steps/checklist'; -import { ReindexingCompletedFlyoutStep } from './steps/completed'; +import { MigrationCompletedFlyoutStep } from './steps/completed'; -interface Props extends ReindexStateContext { +interface Props extends MigrationStateContext { deprecation: EnrichedDeprecationInfo; closeFlyout: () => void; } @@ -51,30 +55,32 @@ const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b'; export const DataStreamReindexFlyout: React.FunctionComponent = ({ cancelReindex, loadDataStreamMetadata, - reindexState, + migrationState, startReindex, + startReadonly, + initMigration, + cancelReadonly, closeFlyout, deprecation, }) => { - const { status, reindexWarnings, errorMessage, meta } = reindexState; - const { index } = deprecation; - + const { status, migrationWarnings, errorMessage, resolutionType, meta } = migrationState; + const { index, correctiveAction } = deprecation; const [flyoutStep, setFlyoutStep] = useState('initializing'); const switchFlyoutStep = useCallback(() => { switch (status) { - case DataStreamReindexStatus.notStarted: { + case DataStreamMigrationStatus.notStarted: { setFlyoutStep('notStarted'); return; } - case DataStreamReindexStatus.failed: - case DataStreamReindexStatus.fetchFailed: - case DataStreamReindexStatus.cancelled: - case DataStreamReindexStatus.inProgress: { + case DataStreamMigrationStatus.failed: + case DataStreamMigrationStatus.fetchFailed: + case DataStreamMigrationStatus.cancelled: + case DataStreamMigrationStatus.inProgress: { setFlyoutStep('inProgress'); return; } - case DataStreamReindexStatus.completed: { + case DataStreamMigrationStatus.completed: { setTimeout(() => { // wait for 1.5 more seconds fur the UI to visually get to 100% setFlyoutStep('completed'); @@ -97,11 +103,21 @@ export const DataStreamReindexFlyout: React.FunctionComponent = ({ await startReindex(); }, [startReindex]); + const onStartReadonly = useCallback(async () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_START_READONLY_CLICK); + await startReadonly(); + }, [startReadonly]); + const onStopReindex = useCallback(async () => { uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_REINDEX_STOP_CLICK); await cancelReindex(); }, [cancelReindex]); + const onStopReadonly = useCallback(async () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_STOP_READONLY_CLICK); + await cancelReadonly(); + }, [cancelReadonly]); + const { docsSizeFormatted, indicesRequiringUpgradeDocsCount, lastIndexCreationDateFormatted } = useMemo(() => { if (!meta) { @@ -143,77 +159,93 @@ export const DataStreamReindexFlyout: React.FunctionComponent = ({ return ( { + initAction={(selectedResolutionType) => { + initMigration(selectedResolutionType); setFlyoutStep('confirm'); }} - reindexState={reindexState} + lastIndexCreationDateFormatted={lastIndexCreationDateFormatted} + meta={meta} + migrationState={migrationState} /> ); } case 'confirm': { - if (!meta) { + if (!meta || !resolutionType) { return ( ); } + return ( - warning.resolutionType === resolutionType + )} meta={meta} + resolutionType={resolutionType} hideWarningsStep={() => { setFlyoutStep('notStarted'); }} - continueReindex={() => { - onStartReindex(); + startAction={() => { + if (resolutionType === 'readonly') { + onStartReadonly(); + } else { + onStartReindex(); + } }} /> ); } case 'inProgress': { - if (!meta) { + if (!meta || !resolutionType) { return ( ); } + return ( { + executeAction={() => { setFlyoutStep('confirm'); }} - reindexState={reindexState} - cancelReindex={onStopReindex} + resolutionType={resolutionType} + migrationState={migrationState} + cancelAction={() => { + if (resolutionType === 'readonly') { + onStopReadonly(); + } else { + onStopReindex(); + } + }} /> ); } case 'completed': { - if (!meta) { - return ( - - ); - } - return ; + return ; } } }, [ flyoutStep, - reindexState, + migrationState, closeFlyout, onStartReindex, onStopReindex, lastIndexCreationDateFormatted, - reindexWarnings, + migrationWarnings, meta, errorMessage, + onStartReadonly, + onStopReadonly, + resolutionType, + initMigration, + correctiveAction, ]); return ( @@ -222,7 +254,7 @@ export const DataStreamReindexFlyout: React.FunctionComponent = ({ @@ -237,7 +269,12 @@ export const DataStreamReindexFlyout: React.FunctionComponent = ({ textStyle="reverse" listItems={[ { - title: 'Reindexing required for indices created on or before', + title: i18n.translate( + 'xpack.upgradeAssistant.dataStream.flyout.container.affectedIndicesCreatedOnOrBefore', + { + defaultMessage: 'Migration required for indices created on or before', + } + ), description: lastIndexCreationDateFormatted, }, ]} @@ -249,7 +286,12 @@ export const DataStreamReindexFlyout: React.FunctionComponent = ({ textStyle="reverse" listItems={[ { - title: 'Size', + title: i18n.translate( + 'xpack.upgradeAssistant.dataStream.flyout.container.indicesDocsSize', + { + defaultMessage: 'Size', + } + ), description: docsSizeFormatted, }, ]} @@ -260,7 +302,12 @@ export const DataStreamReindexFlyout: React.FunctionComponent = ({ textStyle="reverse" listItems={[ { - title: 'Document Count', + title: i18n.translate( + 'xpack.upgradeAssistant.dataStream.flyout.container.indicesDocsCount', + { + defaultMessage: 'Document Count', + } + ), description: indicesRequiringUpgradeDocsCount, }, ]} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/messages.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/messages.tsx index 8766f8bd424e6..43d32e78cafa2 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/messages.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/messages.tsx @@ -8,37 +8,46 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { DataStreamReindexStatus } from '../../../../../../../common/types'; +import { + DataStreamMigrationStatus, + DataStreamResolutionType, +} from '../../../../../../../common/types'; -export const getPrimaryButtonLabel = (status?: DataStreamReindexStatus) => { +export const getPrimaryButtonLabel = ( + status?: DataStreamMigrationStatus, + resolutionType?: DataStreamResolutionType +) => { switch (status) { - case DataStreamReindexStatus.fetchFailed: - case DataStreamReindexStatus.failed: + case DataStreamMigrationStatus.fetchFailed: + case DataStreamMigrationStatus.failed: return ( ); - case DataStreamReindexStatus.inProgress: + case DataStreamMigrationStatus.inProgress: return ( ); - case DataStreamReindexStatus.cancelled: + case DataStreamMigrationStatus.cancelled: return ( ); default: return ( ); } diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/checklist_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/checklist_step.tsx index 0c5ada7a122dd..24a4f928b1f88 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/checklist_step.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/checklist_step.tsx @@ -20,10 +20,13 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DataStreamReindexStatus } from '../../../../../../../../../common/types'; +import { + DataStreamMigrationStatus, + DataStreamResolutionType, +} from '../../../../../../../../../common/types'; import { LoadingState } from '../../../../../../types'; -import type { ReindexState } from '../../../use_reindex_state'; -import { ReindexProgress } from './progress'; +import type { MigrationState } from '../../../use_migration_state'; +import { MigrationProgress } from './progress'; import { useAppContext } from '../../../../../../../app_context'; import { getPrimaryButtonLabel } from '../../messages'; @@ -32,25 +35,26 @@ import { getPrimaryButtonLabel } from '../../messages'; */ export const ChecklistFlyoutStep: React.FunctionComponent<{ closeFlyout: () => void; - reindexState: ReindexState; - startReindex: () => void; - cancelReindex: () => void; -}> = ({ closeFlyout, reindexState, startReindex, cancelReindex }) => { + migrationState: MigrationState; + resolutionType: DataStreamResolutionType; + executeAction: () => void; + cancelAction: () => void; +}> = ({ closeFlyout, migrationState, resolutionType, executeAction, cancelAction }) => { const { services: { api }, } = useAppContext(); - const { loadingState, status, hasRequiredPrivileges } = reindexState; + const { loadingState, status, hasRequiredPrivileges } = migrationState; const loading = - loadingState === LoadingState.Loading || status === DataStreamReindexStatus.inProgress; - const isCompleted = status === DataStreamReindexStatus.completed; - const hasFetchFailed = status === DataStreamReindexStatus.fetchFailed; - const hasReindexingFailed = status === DataStreamReindexStatus.failed; + loadingState === LoadingState.Loading || status === DataStreamMigrationStatus.inProgress; + const isCompleted = status === DataStreamMigrationStatus.completed; + const hasFetchFailed = status === DataStreamMigrationStatus.fetchFailed; + const hasMigrationFailed = status === DataStreamMigrationStatus.failed; const { data: nodes } = api.useLoadNodeDiskSpace(); const showMainButton = !hasFetchFailed && !isCompleted && hasRequiredPrivileges; - const shouldShowCancelButton = showMainButton && status === DataStreamReindexStatus.inProgress; + const shouldShowCancelButton = showMainButton && status === DataStreamMigrationStatus.inProgress; return ( @@ -61,8 +65,8 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ } color="danger" @@ -79,15 +83,15 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ data-test-subj="lowDiskSpaceCallout" title={ } > <> @@ -96,7 +100,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ {nodes.map(({ nodeName, available, nodeId }) => (
  • )} - {(hasFetchFailed || hasReindexingFailed) && ( + {(hasFetchFailed || hasMigrationFailed) && ( <> ) : ( ) } > - {reindexState.errorMessage} + {migrationState.errorMessage} @@ -142,19 +149,19 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{

    - + @@ -167,13 +174,14 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ @@ -183,12 +191,14 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ {getPrimaryButtonLabel(status)} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/progress.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/progress.tsx index 950f2b0d0331e..720407144e69a 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/progress.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/progress.tsx @@ -12,37 +12,39 @@ import { FormattedMessage, FormattedRelativeTime } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { DataStreamReindexStatus } from '../../../../../../../../../common/types'; -import type { ReindexState } from '../../../use_reindex_state'; -import { StepProgress, StepProgressStep } from '../../../../reindex/flyout/step_progress'; +import { DataStreamMigrationStatus } from '../../../../../../../../../common/types'; +import type { MigrationState } from '../../../use_migration_state'; import { getDataStreamReindexProgress } from '../../../../../../../lib/utils'; -import { ReindexingDocumentsStepTitle } from './progress_title'; +import { MigrateDocumentsStepTitle } from './progress_title'; import { CancelLoadingState } from '../../../../../../types'; +import { StepProgress, type StepProgressStep } from '../../../../../common/step_progress'; interface Props { - reindexState: ReindexState; + migrationState: MigrationState; } /** * Displays a list of steps in the reindex operation, the current status, a progress bar, * and any error messages that are encountered. */ -export const ReindexProgress: React.FunctionComponent = (props) => { - const { status, reindexTaskPercComplete, cancelLoadingState, taskStatus } = props.reindexState; +export const MigrationProgress: React.FunctionComponent = (props) => { + const { status, taskPercComplete, cancelLoadingState, taskStatus, resolutionType } = + props.migrationState; // The reindexing step is special because it generally lasts longer and can be cancelled mid-flight const reindexingDocsStep = { title: ( - + ), } as StepProgressStep; const inProgress = - status === DataStreamReindexStatus.inProgress || status === DataStreamReindexStatus.completed; + status === DataStreamMigrationStatus.inProgress || + status === DataStreamMigrationStatus.completed; let euiProgressColor = 'subdued'; @@ -55,21 +57,21 @@ export const ReindexProgress: React.FunctionComponent = (props) => { ) { reindexingDocsStep.status = 'inProgress'; euiProgressColor = 'subdued'; - } else if (status === DataStreamReindexStatus.failed) { + } else if (status === DataStreamMigrationStatus.failed) { reindexingDocsStep.status = 'failed'; euiProgressColor = 'danger'; } else if ( - status === DataStreamReindexStatus.cancelled || + status === DataStreamMigrationStatus.cancelled || cancelLoadingState === CancelLoadingState.Success ) { reindexingDocsStep.status = 'cancelled'; } else if (status === undefined) { reindexingDocsStep.status = 'incomplete'; euiProgressColor = 'subdued'; - } else if (status === DataStreamReindexStatus.inProgress) { + } else if (status === DataStreamMigrationStatus.inProgress) { reindexingDocsStep.status = 'inProgress'; euiProgressColor = 'primary'; - } else if (status === DataStreamReindexStatus.completed) { + } else if (status === DataStreamMigrationStatus.completed) { reindexingDocsStep.status = 'complete'; euiProgressColor = 'success'; } else { @@ -79,7 +81,7 @@ export const ReindexProgress: React.FunctionComponent = (props) => { } const progressPercentage = inProgress - ? getDataStreamReindexProgress(status, reindexTaskPercComplete) + ? getDataStreamReindexProgress(status, taskPercComplete) : undefined; const showProgressValueText = inProgress; const progressMaxValue = inProgress ? 100 : undefined; @@ -89,15 +91,17 @@ export const ReindexProgress: React.FunctionComponent = (props) => {

    - {status === DataStreamReindexStatus.inProgress ? ( + {status === DataStreamMigrationStatus.inProgress ? ( ) : ( )}

    @@ -110,7 +114,7 @@ export const ReindexProgress: React.FunctionComponent = (props) => { label={ taskStatus ? ( = (props) => { {!taskStatus && (

    @@ -151,11 +155,11 @@ export const ReindexProgress: React.FunctionComponent = (props) => {

    {i18n.translate( - 'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.failedTitle', + 'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.progressStep.failedTitle', { defaultMessage: - '{count, plural, =1 {# Index} other {# Indices}} failed to reindex.', - values: { count: taskStatus.errorsCount }, + '{count, plural, =1 {# Index} other {# Indices}} failed to get {resolutionType, select, reindex {reindexed} readonly {marked as read-only} other {migrated}}.', + values: { count: taskStatus.errorsCount, resolutionType }, } )}

    @@ -166,11 +170,11 @@ export const ReindexProgress: React.FunctionComponent = (props) => {

    {i18n.translate( - 'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.completeTitle', + 'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.progressStep.completeTitle', { defaultMessage: - '{count, plural, =1 {# Index} other {# Indices}} successfully reindexed.', - values: { count: taskStatus.successCount }, + '{count, plural, =1 {# Index} other {# Indices}} successfully {resolutionType, select, reindex {reindexed} readonly {marked as read-only} other {migrated}}.', + values: { count: taskStatus.successCount, resolutionType }, } )}

    @@ -180,11 +184,11 @@ export const ReindexProgress: React.FunctionComponent = (props) => {

    {i18n.translate( - 'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.inProgressTitle', + 'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.progressStep.inProgressTitle', { defaultMessage: - '{count, plural, =1 {# Index} other {# Indices}} currently reindexing.', - values: { count: taskStatus.inProgressCount }, + '{count, plural, =1 {# Index} other {# Indices}} currently getting {resolutionType, select, reindex {reindexed} readonly {marked as read-only} other {migrated}}.', + values: { count: taskStatus.inProgressCount, resolutionType }, } )}

    @@ -194,7 +198,7 @@ export const ReindexProgress: React.FunctionComponent = (props) => {

    {i18n.translate( - 'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.pendingTitle', + 'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.progressStep.pendingTitle', { defaultMessage: '{count, plural, =1 {# Index} other {# Indices}} waiting to start.', diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/progress_title.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/progress_title.tsx index 32e72f787aaf4..2609e0c81f536 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/progress_title.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/checklist/progress_title.tsx @@ -9,18 +9,18 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { CancelLoadingState } from '../../../../../../types'; -import { DataStreamReindexStatus } from '../../../../../../../../../common/types'; -import type { ReindexState } from '../../../use_reindex_state'; +import { DataStreamMigrationStatus } from '../../../../../../../../../common/types'; +import type { MigrationState } from '../../../use_migration_state'; -export const ReindexingDocumentsStepTitle: React.FunctionComponent<{ - reindexState: ReindexState; -}> = ({ reindexState: { status, cancelLoadingState } }) => { +export const MigrateDocumentsStepTitle: React.FunctionComponent<{ + migrationState: MigrationState; +}> = ({ migrationState: { status, cancelLoadingState, resolutionType } }) => { switch (cancelLoadingState) { case CancelLoadingState.Requested: case CancelLoadingState.Loading: { return ( ); @@ -28,7 +28,7 @@ export const ReindexingDocumentsStepTitle: React.FunctionComponent<{ case CancelLoadingState.Success: { return ( ); @@ -36,56 +36,61 @@ export const ReindexingDocumentsStepTitle: React.FunctionComponent<{ case CancelLoadingState.Error: { return ( ); } } switch (status) { - case DataStreamReindexStatus.inProgress: { + case DataStreamMigrationStatus.inProgress: { return ( ); } - case DataStreamReindexStatus.failed: + case DataStreamMigrationStatus.failed: return ( ); - case DataStreamReindexStatus.fetchFailed: + case DataStreamMigrationStatus.fetchFailed: return ( ); - case DataStreamReindexStatus.cancelled: + case DataStreamMigrationStatus.cancelled: return ( ); - case DataStreamReindexStatus.completed: + case DataStreamMigrationStatus.completed: return ( ); - case DataStreamReindexStatus.notStarted: + case DataStreamMigrationStatus.notStarted: default: { return ( ); } diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/completed/completed_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/completed/completed_step.tsx index dd7751e25489a..66aff922f177a 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/completed/completed_step.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/completed/completed_step.tsx @@ -9,30 +9,37 @@ import React from 'react'; import { EuiFlyoutBody, EuiSpacer, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { DataStreamMetadata } from '../../../../../../../../../common/types'; +import type { + DataStreamMetadata, + DataStreamResolutionType, +} from '../../../../../../../../../common/types'; interface Props { - meta: DataStreamMetadata; + meta?: DataStreamMetadata | null; + resolutionType?: DataStreamResolutionType; } -export const ReindexingCompletedFlyoutStep: React.FunctionComponent = ({ meta }: Props) => { +export const MigrationCompletedFlyoutStep: React.FunctionComponent = ({ + meta, + resolutionType, +}: Props) => { return ( <>

    diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/completed/index.ts b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/completed/index.ts index 14cc2b49ea290..af70e9571218b 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/completed/index.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/completed/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ReindexingCompletedFlyoutStep } from './completed_step'; +export { MigrationCompletedFlyoutStep } from './completed_step'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/callouts.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/callouts.tsx new file mode 100644 index 0000000000000..ecb3e90c6f6d9 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/callouts.tsx @@ -0,0 +1,54 @@ +/* + * 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 React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut } from '@elastic/eui'; + +export const ReindexWarningCallout: React.FunctionComponent<{}> = () => { + return ( + + } + color="warning" + iconType="warning" + > +

    + +

    +
    + ); +}; + +export const ReadonlyWarningCallout: React.FunctionComponent<{}> = () => { + return ( + + } + color="warning" + iconType="warning" + > +

    + +

    +
    + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/confirm_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/confirm_step.tsx index ec480db552e22..ba4a929afa292 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/confirm_step.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/confirm_step.tsx @@ -10,59 +10,60 @@ import React, { useState } from 'react'; import { EuiButton, EuiButtonEmpty, - EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiFlyoutBody, EuiFlyoutFooter, + EuiLink, EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; - +import { i18n } from '@kbn/i18n'; import { - DataStreamReindexWarning, - DataStreamReindexWarningTypes, + DataStreamMigrationWarning, + DataStreamWarningTypes, DataStreamMetadata, + DataStreamResolutionType, } from '../../../../../../../../../common/types'; import { useAppContext } from '../../../../../../../app_context'; import { IncompatibleDataInDataStreamWarningCheckbox, WarningCheckboxProps, -} from './warning_step_checkbox'; + AffectExistingSetupsWarningCheckbox, +} from './warnings'; +import { ReindexWarningCallout, ReadonlyWarningCallout } from './callouts'; interface CheckedIds { [id: string]: boolean; } const warningToComponentMap: Record< - DataStreamReindexWarningTypes, + DataStreamWarningTypes, React.FunctionComponent > = { incompatibleDataStream: IncompatibleDataInDataStreamWarningCheckbox, + affectExistingSetups: AffectExistingSetupsWarningCheckbox, }; -export const idForWarning = (id: number) => `reindexWarning-${id}`; -interface WarningsConfirmationFlyoutProps { - hideWarningsStep: () => void; - continueReindex: () => void; - warnings: DataStreamReindexWarning[]; - meta: DataStreamMetadata; -} +export const idForWarning = (id: number) => `migrationWarning-${id}`; /** - * Displays warning text about destructive changes required to reindex this index. The user + * Displays warning text about changes required to migrate this data stream. The user * must acknowledge each change before being allowed to proceed. */ -export const ConfirmReindexingFlyoutStep: React.FunctionComponent< - WarningsConfirmationFlyoutProps -> = ({ warnings, hideWarningsStep, continueReindex, meta }) => { +export const ConfirmMigrationFlyoutStep: React.FunctionComponent<{ + hideWarningsStep: () => void; + startAction: () => void; + resolutionType: DataStreamResolutionType; + warnings: DataStreamMigrationWarning[]; + meta: DataStreamMetadata; +}> = ({ warnings, hideWarningsStep, startAction, resolutionType, meta }) => { const { services: { core: { docLinks }, }, } = useAppContext(); const { links } = docLinks; - const [checkedIds, setCheckedIds] = useState( warnings.reduce((initialCheckedIds, warning, index) => { initialCheckedIds[idForWarning(index)] = false; @@ -84,36 +85,75 @@ export const ConfirmReindexingFlyoutStep: React.FunctionComponent< })); }; + const startActionButtonLabel = + resolutionType === 'reindex' + ? i18n.translate( + 'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.startActionButtonLabel', + { + defaultMessage: 'Start reindexing', + } + ) + : i18n.translate( + 'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.startActionButtonLabel', + { + defaultMessage: 'Mark all read-only', + } + ); + + const actionClarification = + resolutionType === 'reindex' ? ( + <> +

    + +

    + +

    + + + + ), + }} + /> +

    + + ) : ( +

    + +

    + ); + return ( <> {warnings.length > 0 && ( <> - - } - color="warning" - iconType="warning" - > -

    - -

    -
    + {resolutionType === 'reindex' && } + {resolutionType === 'readonly' && } -

    - -

    + {actionClarification} {warnings.map((warning, index) => { const WarningCheckbox = warningToComponentMap[warning.warningType]; @@ -137,17 +177,14 @@ export const ConfirmReindexingFlyoutStep: React.FunctionComponent< - - + + {startActionButtonLabel}
    diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/index.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/index.tsx index de9d8a7c4b12f..c702378283bb7 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/index.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/index.tsx @@ -5,4 +5,4 @@ * 2.0. */ -export { ConfirmReindexingFlyoutStep } from './confirm_step'; +export { ConfirmMigrationFlyoutStep } from './confirm_step'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/existing_setups.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/existing_setups.tsx new file mode 100644 index 0000000000000..c1a5eb3205b3c --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/existing_setups.tsx @@ -0,0 +1,31 @@ +/* + * 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 React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { WarningCheckbox, WarningCheckboxProps } from './warning_step_checkbox'; + +export const AffectExistingSetupsWarningCheckbox: React.FunctionComponent = ({ + isChecked, + onChange, + id, +}) => { + return ( + + } + description={null} + /> + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/incompatible_data.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/incompatible_data.tsx new file mode 100644 index 0000000000000..0ad2b1b347a5b --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/incompatible_data.tsx @@ -0,0 +1,29 @@ +/* + * 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 React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { WarningCheckbox, WarningCheckboxProps } from './warning_step_checkbox'; + +export const IncompatibleDataInDataStreamWarningCheckbox: React.FunctionComponent< + WarningCheckboxProps +> = ({ isChecked, onChange, id }) => { + return ( + + } + description={null} + /> + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/index.ts b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/index.ts new file mode 100644 index 0000000000000..4b4d43f6f7431 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { IncompatibleDataInDataStreamWarningCheckbox } from './incompatible_data'; + +export { AffectExistingSetupsWarningCheckbox } from './existing_setups'; + +export type { WarningCheckboxProps } from './warning_step_checkbox'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warning_step_checkbox.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/warning_step_checkbox.tsx similarity index 64% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warning_step_checkbox.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/warning_step_checkbox.tsx index daa452fcbb96c..9e789f4c1169d 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warning_step_checkbox.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/confirm/warnings/warning_step_checkbox.tsx @@ -18,19 +18,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { DocLinksStart } from '@kbn/core/public'; -import { - DataStreamReindexWarning, - DataStreamReindexWarningTypes, -} from '../../../../../../../../../common/types'; - -export const hasReindexWarning = ( - warnings: DataStreamReindexWarning[], - warningType: DataStreamReindexWarningTypes -): boolean => { - return Boolean(warnings.find((warning) => warning.warningType === warningType)); -}; -const WarningCheckbox: React.FunctionComponent<{ +export const WarningCheckbox: React.FunctionComponent<{ isChecked: boolean; warningId: string; label: React.ReactNode; @@ -55,7 +44,7 @@ const WarningCheckbox: React.FunctionComponent<{ } @@ -82,22 +71,3 @@ export interface WarningCheckboxProps { docLinks: DocLinksStart['links']; id: string; } - -export const IncompatibleDataInDataStreamWarningCheckbox: React.FunctionComponent< - WarningCheckboxProps -> = ({ isChecked, onChange, id }) => { - return ( - - } - description={null} - /> - ); -}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/details/details_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/details/details_step.tsx index ca507cbd7aa10..08dbd0ac13da2 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/details/details_step.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/details/details_step.tsx @@ -23,13 +23,14 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { DataStreamMetadata, - DataStreamReindexStatus, + DataStreamMigrationStatus, + DataStreamResolutionType, + DataStreamsAction, } from '../../../../../../../../../common/types'; import { LoadingState } from '../../../../../../types'; -import type { ReindexState } from '../../../use_reindex_state'; +import type { MigrationState } from '../../../use_migration_state'; import { useAppContext } from '../../../../../../../app_context'; import { DurationClarificationCallOut } from './warnings_callout'; -import { getPrimaryButtonLabel } from '../../messages'; /** * Displays a flyout that shows the current reindexing status for a given index. @@ -37,11 +38,19 @@ import { getPrimaryButtonLabel } from '../../messages'; export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{ closeFlyout: () => void; - reindexState: ReindexState; - startReindex: () => void; + migrationState: MigrationState; + correctiveAction: DataStreamsAction; + initAction: (resolutionType: DataStreamResolutionType) => void; lastIndexCreationDateFormatted: string; meta: DataStreamMetadata; -}> = ({ closeFlyout, reindexState, startReindex, lastIndexCreationDateFormatted, meta }) => { +}> = ({ + closeFlyout, + migrationState, + initAction, + lastIndexCreationDateFormatted, + correctiveAction, + meta, +}) => { const { services: { api, @@ -49,12 +58,15 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{ }, } = useAppContext(); - const { loadingState, status, hasRequiredPrivileges } = reindexState; + const { loadingState, status, hasRequiredPrivileges } = migrationState; const loading = - loadingState === LoadingState.Loading || status === DataStreamReindexStatus.inProgress; - const isCompleted = status === DataStreamReindexStatus.completed; - const hasFetchFailed = status === DataStreamReindexStatus.fetchFailed; - const hasReindexingFailed = status === DataStreamReindexStatus.failed; + loadingState === LoadingState.Loading || status === DataStreamMigrationStatus.inProgress; + const isCompleted = status === DataStreamMigrationStatus.completed; + const hasFetchFailed = status === DataStreamMigrationStatus.fetchFailed; + const hasMigrationFailed = status === DataStreamMigrationStatus.failed; + + const readOnlyExcluded = correctiveAction.metadata.excludedActions?.includes('readOnly'); + const reindexExcluded = correctiveAction.metadata.excludedActions?.includes('reindex'); const { data: nodes } = api.useLoadNodeDiskSpace(); @@ -73,8 +85,8 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{ } color="danger" @@ -91,15 +103,15 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{ data-test-subj="lowDiskSpaceCallout" title={ } > <> @@ -108,7 +120,7 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{ {nodes.map(({ nodeName, available, nodeId }) => (
  • )} - {(hasFetchFailed || hasReindexingFailed) && ( + {(hasFetchFailed || hasMigrationFailed) && ( <> ) : ( ) } > - {reindexState.errorMessage} + {migrationState.errorMessage} @@ -153,7 +165,7 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{

      - -
    • - -
        + {!readOnlyExcluded && ( +
      • +
      • + )} + {!reindexExcluded && ( +
      • -
      -
    • +
        + + +
      + + )}

    @@ -223,24 +240,49 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{ - {!hasFetchFailed && !isCompleted && hasRequiredPrivileges && ( + {!hasFetchFailed && !isCompleted && hasRequiredPrivileges && !reindexExcluded && ( initAction('reindex')} isLoading={loading} - disabled={loading || !hasRequiredPrivileges} - data-test-subj="startReindexingButton" + disabled={loading || !hasRequiredPrivileges || reindexExcluded} + data-test-subj="startDataStreamReindexingButton" > - {getPrimaryButtonLabel(status)} + + + + )} + {!readOnlyExcluded && ( + + initAction('readonly')} + disabled={!hasRequiredPrivileges || readOnlyExcluded} + data-test-subj="startDataStreamReadonlyButton" + > + )} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/details/warnings_callout.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/details/warnings_callout.tsx index 45df6e51a667c..fbd5dc4b866f1 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/details/warnings_callout.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/details/warnings_callout.tsx @@ -22,20 +22,26 @@ export const DurationClarificationCallOut: React.FunctionComponent = ({



    +
    +
    + diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/initializing/initializing_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/initializing/initializing_step.tsx index 1fb5ce6bda347..5646193575f1b 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/initializing/initializing_step.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/flyout/steps/initializing/initializing_step.tsx @@ -44,12 +44,12 @@ export const InitializingFlyoutStep: React.FunctionComponent {hasInitializingError ? ( ) : ( )} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/readonly_state.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/readonly_state.tsx new file mode 100644 index 0000000000000..5c17142b4bc07 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/readonly_state.tsx @@ -0,0 +1,106 @@ +/* + * 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 { chunk } from 'lodash'; +import { + DataStreamMetadata, + DataStreamMigrationStatus, + DataStreamMigrationOperation, +} from '../../../../../../common/types'; +import { ApiService } from '../../../../lib/api'; + +interface ReadOnlyExecuteResponse { + migrationOp: DataStreamMigrationOperation; +} + +const DEFAULT_BATCH_SIZE = 10; + +export async function* readOnlyExecute( + dataStreamName: string, + meta: DataStreamMetadata | null, + api: ApiService, + batchSize: number = DEFAULT_BATCH_SIZE +): AsyncGenerator { + const { indicesRequiringUpgrade } = meta || {}; + + const startTimeMs = +Date.now(); + + if (!indicesRequiringUpgrade || !indicesRequiringUpgrade.length) { + return { + migrationOp: { + status: DataStreamMigrationStatus.completed, + resolutionType: 'readonly', + taskPercComplete: 1, + progressDetails: { + startTimeMs, + successCount: 0, + pendingCount: 0, + inProgressCount: 0, + errorsCount: 0, + }, + }, + }; + } + + let processedCount = 0; + const batches = chunk(indicesRequiringUpgrade, batchSize); + + try { + for (const batch of batches) { + const { error } = await api.markIndicesAsReadOnly(dataStreamName, batch); + if (error) { + throw error; + } + + processedCount += batch.length; + + const status = + processedCount >= indicesRequiringUpgrade.length + ? DataStreamMigrationStatus.completed + : DataStreamMigrationStatus.inProgress; + const taskPercComplete = processedCount / indicesRequiringUpgrade.length; + + yield { + migrationOp: { + resolutionType: 'readonly', + status, + taskPercComplete, + progressDetails: { + startTimeMs, + successCount: processedCount, + pendingCount: indicesRequiringUpgrade.length - processedCount, + inProgressCount: batch.length, + errorsCount: 0, + }, + }, + }; + } + } catch (error) { + return { + migrationOp: { + resolutionType: 'readonly', + status: DataStreamMigrationStatus.failed, + errorMessage: error.message || 'Unknown error occurred', + }, + }; + } + + return { + migrationOp: { + resolutionType: 'readonly', + status: DataStreamMigrationStatus.completed, + taskPercComplete: 1, + progressDetails: { + startTimeMs, + successCount: indicesRequiringUpgrade.length, + pendingCount: 0, + inProgressCount: 0, + errorsCount: 0, + }, + }, + }; +} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/resolution_table_cell.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/resolution_table_cell.tsx index 2f12c1411e731..ce5c641b7366a 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/resolution_table_cell.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/resolution_table_cell.tsx @@ -16,81 +16,146 @@ import { EuiFlexItem, EuiToolTip, } from '@elastic/eui'; -import { DataStreamReindexStatus } from '../../../../../../common/types'; +import { + DataStreamMigrationStatus, + DataStreamResolutionType, + DataStreamsAction, +} from '../../../../../../common/types'; import { getDataStreamReindexProgressLabel } from '../../../../lib/utils'; import { LoadingState } from '../../../types'; -import { useDataStreamReindexContext } from './context'; +import { useDataStreamMigrationContext } from './context'; + +const getI18nTexts = ( + resolutionType?: DataStreamResolutionType, + excludedActions: Array<'readOnly' | 'reindex'> = [] +) => { + const resolutionAction = excludedActions.includes('readOnly') + ? 'reindex' + : excludedActions.includes('reindex') + ? 'readOnly' + : 'readOnlyOrReindex'; + + const resolutionTexts = { + readOnlyOrReindex: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionReadOnlyOrReindexLabel', + { + defaultMessage: 'Mark as read-only, or reindex', + } + ), + readOnly: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionReadOnlyLabel', + { + defaultMessage: 'Mark as read-only', + } + ), + reindex: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionReindexLabel', + { + defaultMessage: 'Reindex', + } + ), + }; + + const resolutionTooltipLabels = { + readOnlyOrReindex: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipReadOnlyOrReindexLabel', + { + defaultMessage: + 'Resolve this issue by reindexing this data stream or marking its indices as read-only. This issue can be resolved automatically.', + } + ), + readOnly: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipReadOnlyLabel', + { + defaultMessage: + 'Resolve this issue by marking its indices as read-only. This issue can be resolved automatically.', + } + ), + reindex: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipReindexLabel', + { + defaultMessage: + 'Resolve this issue by reindexing this data stream. This issue can be resolved automatically.', + } + ), + }; -const i18nTexts = { - reindexLoadingStatusText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.dataStream.reindexLoadingStatusText', - { - defaultMessage: 'Loading status…', - } - ), - reindexInProgressText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.dataStream.reindexInProgressText', - { - defaultMessage: 'Reindexing in progress…', - } - ), - reindexCompleteText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.dataStream.reindexCompleteText', - { - defaultMessage: 'Reindex complete', - } - ), - reindexFailedText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.dataStream.reindexFailedText', - { - defaultMessage: 'Reindex failed', - } - ), - reindexFetchFailedText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.dataStream.reindexFetchFailedText', - { - defaultMessage: 'Reindex status not available', - } - ), - reindexCanceledText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.dataStream.reindexCanceledText', - { - defaultMessage: 'Reindex cancelled', - } - ), - resolutionText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLabel', - { - defaultMessage: 'Reindex', - } - ), - resolutionTooltipLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipLabel', - { - defaultMessage: - 'Resolve this issue by reindexing this data stream. This issue can be resolved automatically.', - } - ), + return { + loadingStatusText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLoadingStatusText', + { + defaultMessage: 'Loading status…', + } + ), + resolutionInProgressText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionInProgressText', + { + defaultMessage: + '{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} in progress…', + values: { resolutionType }, + } + ), + resolutionCompleteText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionCompleteText', + { + defaultMessage: + '{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} complete', + values: { resolutionType }, + } + ), + resolutionFailedText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resulutionFailedText', + { + defaultMessage: + '{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} failed', + values: { resolutionType }, + } + ), + resolutionFetchFailedText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionFetchFailedText', + { + defaultMessage: + '{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} status not available', + values: { resolutionType }, + } + ), + reindexCanceledText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionCanceledText', + { + defaultMessage: + '{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} cancelled', + values: { resolutionType }, + } + ), + resolutionText: resolutionTexts[resolutionAction], + resolutionTooltipLabel: resolutionTooltipLabels[resolutionAction], + }; }; -export const DataStreamReindexResolutionCell: React.FunctionComponent = () => { - const { reindexState } = useDataStreamReindexContext(); +export const DataStreamReindexResolutionCell: React.FunctionComponent<{ + correctiveAction: DataStreamsAction; +}> = ({ correctiveAction }) => { + const { migrationState } = useDataStreamMigrationContext(); + const i18nTexts = getI18nTexts( + migrationState.resolutionType, + correctiveAction.metadata.excludedActions + ); - if (reindexState.loadingState === LoadingState.Loading) { + if (migrationState.loadingState === LoadingState.Loading) { return ( - {i18nTexts.reindexLoadingStatusText} + {i18nTexts.loadingStatusText} ); } - switch (reindexState.status) { - case DataStreamReindexStatus.inProgress: + switch (migrationState.status) { + case DataStreamMigrationStatus.inProgress: return ( @@ -98,45 +163,45 @@ export const DataStreamReindexResolutionCell: React.FunctionComponent = () => { - {i18nTexts.reindexInProgressText}{' '} + {i18nTexts.resolutionInProgressText}{' '} {getDataStreamReindexProgressLabel( - reindexState.status, - reindexState.reindexTaskPercComplete + migrationState.status, + migrationState.taskPercComplete )} ); - case DataStreamReindexStatus.completed: + case DataStreamMigrationStatus.completed: return ( - {i18nTexts.reindexCompleteText} + {i18nTexts.resolutionCompleteText} ); - case DataStreamReindexStatus.failed: + case DataStreamMigrationStatus.failed: return ( - {i18nTexts.reindexFailedText} + {i18nTexts.resolutionFailedText} ); - case DataStreamReindexStatus.fetchFailed: + case DataStreamMigrationStatus.fetchFailed: return ( - {i18nTexts.reindexFetchFailedText} + {i18nTexts.resolutionFetchFailedText} ); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/table_row.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/table_row.tsx index 3cfcce5f4a208..7fff63c23baa4 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/table_row.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/table_row.tsx @@ -8,7 +8,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { EuiTableRowCell } from '@elastic/eui'; import { METRIC_TYPE } from '@kbn/analytics'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { DataStreamsAction, EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GlobalFlyout } from '../../../../../shared_imports'; import { useAppContext } from '../../../../app_context'; import { @@ -20,7 +20,7 @@ import { DeprecationTableColumns } from '../../../types'; import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells'; import { DataStreamReindexResolutionCell } from './resolution_table_cell'; import { DataStreamReindexFlyout } from './flyout'; -import { DataStreamReindexStatusProvider, useDataStreamReindexContext } from './context'; +import { DataStreamMigrationStatusProvider, useDataStreamMigrationContext } from './context'; const { useGlobalFlyout } = GlobalFlyout; @@ -34,7 +34,7 @@ const DataStreamTableRowCells: React.FunctionComponent = ({ deprecation, }) => { const [showFlyout, setShowFlyout] = useState(false); - const dataStreamContext = useDataStreamReindexContext(); + const dataStreamContext = useDataStreamMigrationContext(); const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = useGlobalFlyout(); @@ -83,7 +83,11 @@ const DataStreamTableRowCells: React.FunctionComponent = ({ fieldName={field} openFlyout={() => setShowFlyout(true)} deprecation={deprecation} - resolutionTableCell={} + resolutionTableCell={ + + } /> ); @@ -98,8 +102,8 @@ export const DataStreamTableRow: React.FunctionComponent = (props } = useAppContext(); return ( - + - + ); }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/use_migration_state.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/use_migration_state.tsx new file mode 100644 index 0000000000000..8c3eec2834372 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/use_migration_state.tsx @@ -0,0 +1,363 @@ +/* + * 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 { useRef, useCallback, useState, useEffect } from 'react'; + +import { + DataStreamMigrationStatus, + DataStreamMigrationWarning, + DataStreamMetadata, + DataStreamReindexStatusResponse, + DataStreamProgressDetails, + DataStreamResolutionType, + ResponseError, +} from '../../../../../../common/types'; +import { CancelLoadingState, LoadingState } from '../../../types'; +import { ApiService } from '../../../../lib/api'; +import { readOnlyExecute } from './readonly_state'; + +const POLL_INTERVAL = 1000; + +export interface MigrationState { + loadingState: LoadingState; + cancelLoadingState?: CancelLoadingState; + + resolutionType?: DataStreamResolutionType; + status?: DataStreamMigrationStatus; + taskPercComplete: number | null; + errorMessage: string | null; + migrationWarnings?: DataStreamMigrationWarning[]; + hasRequiredPrivileges?: boolean; + taskStatus?: DataStreamProgressDetails; + + meta: DataStreamMetadata | null; +} + +const getMigrationState = ( + migrationState: MigrationState, + { + migrationOp, + warnings, + hasRequiredPrivileges, + meta: updatedMeta, + }: DataStreamReindexStatusResponse & { meta?: DataStreamMetadata | null } +) => { + const newMigrationState: MigrationState = { + ...migrationState, + // @ts-expect-error - resolutionType does non exist in all migration states. + resolutionType: migrationOp?.resolutionType || migrationState.resolutionType, + meta: updatedMeta || migrationState.meta, + loadingState: LoadingState.Success, + }; + + if (warnings) { + newMigrationState.migrationWarnings = warnings; + } + + if (hasRequiredPrivileges !== undefined) { + newMigrationState.hasRequiredPrivileges = hasRequiredPrivileges; + } + + if (migrationOp) { + newMigrationState.status = migrationOp.status; + + if (migrationOp.status === DataStreamMigrationStatus.notStarted) { + return newMigrationState; + } + + if (migrationOp.status === DataStreamMigrationStatus.failed) { + newMigrationState.errorMessage = migrationOp.errorMessage; + return newMigrationState; + } + + if ( + migrationOp.status === DataStreamMigrationStatus.inProgress || + migrationOp.status === DataStreamMigrationStatus.completed + ) { + newMigrationState.taskStatus = migrationOp.progressDetails; + newMigrationState.taskPercComplete = migrationOp.taskPercComplete; + } + + if ( + migrationState.cancelLoadingState === CancelLoadingState.Requested && + migrationOp.status === DataStreamMigrationStatus.inProgress + ) { + newMigrationState.cancelLoadingState = CancelLoadingState.Loading; + } + } + + return newMigrationState; +}; + +export const useMigrationStatus = ({ + dataStreamName, + api, +}: { + dataStreamName: string; + api: ApiService; +}) => { + const [migrationState, setMigrationState] = useState({ + loadingState: LoadingState.Loading, + errorMessage: null, + taskPercComplete: null, + taskStatus: undefined, + meta: null, + }); + + const pollIntervalIdRef = useRef | null>(null); + const readonlyState = useRef | null>(null); + const isMounted = useRef(false); + + const clearPollInterval = useCallback(() => { + if (pollIntervalIdRef.current) { + clearTimeout(pollIntervalIdRef.current); + pollIntervalIdRef.current = null; + } + }, []); + + const pollingFunction = useCallback( + async (resolutionType?: DataStreamResolutionType) => { + clearPollInterval(); + try { + if (resolutionType === 'readonly' && !readonlyState.current) { + return; + } + + let data: DataStreamReindexStatusResponse | null = null; + let error: ResponseError | null = null; + if (resolutionType === 'readonly') { + if (!readonlyState.current) { + throw new Error('Readonly state not initialized'); + } + + const { value } = await readonlyState.current.next(); + + data = value; + } else { + const results = await api.getDataStreamMigrationStatus(dataStreamName); + data = results.data; + error = results.error; + } + + if (error) { + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + loadingState: LoadingState.Error, + errorMessage: error!.message.toString(), + status: DataStreamMigrationStatus.fetchFailed, + }; + }); + + return; + } + + if (!data) { + return; + } + + setMigrationState((prevValue: MigrationState) => { + return getMigrationState(prevValue, data!); + }); + + if (data.migrationOp && data.migrationOp.status === DataStreamMigrationStatus.inProgress) { + // Only keep polling if it exists and is in progress. + pollIntervalIdRef.current = setTimeout( + () => pollingFunction(migrationState.resolutionType), + POLL_INTERVAL + ); + } + } catch (error) { + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + loadingState: LoadingState.Error, + errorMessage: error.message.toString(), + status: DataStreamMigrationStatus.fetchFailed, + }; + }); + } + }, + + [clearPollInterval, api, dataStreamName, migrationState.resolutionType] + ); + + const updateStatus = useCallback(async () => { + return pollingFunction(migrationState.resolutionType); + }, [pollingFunction, migrationState.resolutionType]); + + const startReindex = useCallback(async () => { + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + status: DataStreamMigrationStatus.inProgress, + taskPercComplete: null, + errorMessage: null, + cancelLoadingState: undefined, + }; + }); + + if (migrationState.status === DataStreamMigrationStatus.failed) { + try { + await api.cancelDataStreamReindexTask(dataStreamName); + } catch (_) { + // if the task has already failed, attempt to cancel the task + // before attempting to start the reindexing again. + } + } + + const { data: migrationOp, error } = await api.startDataStreamReindexTask(dataStreamName); + + if (error) { + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + loadingState: LoadingState.Error, + errorMessage: error.message.toString(), + status: DataStreamMigrationStatus.failed, + }; + }); + return; + } + + setMigrationState((prevValue: MigrationState) => { + return getMigrationState(prevValue, { migrationOp, meta: prevValue.meta }); + }); + updateStatus(); + }, [api, dataStreamName, updateStatus, migrationState.status]); + + const loadDataStreamMetadata = useCallback(async () => { + try { + const { data, error } = await api.getDataStreamMetadata(dataStreamName); + + if (error) { + throw error; + } + + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + loadingState: LoadingState.Success, + meta: data || null, + }; + }); + } catch (error) { + setMigrationState((prevValue: MigrationState) => { + // if state is completed, we don't need to update the meta + if (prevValue.status === DataStreamMigrationStatus.completed) { + return prevValue; + } + + return { + ...prevValue, + loadingState: LoadingState.Error, + errorMessage: error.message.toString(), + status: DataStreamMigrationStatus.failed, + }; + }); + } + }, [api, dataStreamName]); + + const cancelReindex = useCallback(async () => { + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + cancelLoadingState: CancelLoadingState.Requested, + }; + }); + try { + const { error } = await api.cancelDataStreamReindexTask(dataStreamName); + + if (error) { + throw error; + } + + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + cancelLoadingState: CancelLoadingState.Success, + status: DataStreamMigrationStatus.cancelled, + }; + }); + } catch (error) { + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + cancelLoadingState: CancelLoadingState.Error, + }; + }); + } + }, [api, dataStreamName]); + + const startReadonly = useCallback(async () => { + /** + * Here we jsut mark the status as in progress for the polling function + * to start executing the reindexing. + */ + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + resolutionType: 'readonly', + status: DataStreamMigrationStatus.inProgress, + taskPercComplete: null, + }; + }); + + readonlyState.current = readOnlyExecute(dataStreamName, migrationState.meta, api); + + pollingFunction('readonly'); + }, [api, dataStreamName, migrationState, pollingFunction]); + + const cancelReadonly = useCallback(async () => { + readonlyState.current = null; + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + resolutionType: undefined, + cancelLoadingState: CancelLoadingState.Success, + status: DataStreamMigrationStatus.cancelled, + }; + }); + }, []); + + const initMigration = useCallback((resolutionType: DataStreamResolutionType) => { + setMigrationState((prevValue: MigrationState) => { + return { + ...prevValue, + resolutionType, + status: DataStreamMigrationStatus.notStarted, + }; + }); + }, []); + + useEffect(() => { + updateStatus(); + }, [updateStatus]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + + // Clean up on unmount. + clearPollInterval(); + }; + }, [clearPollInterval]); + + return { + migrationState, + loadDataStreamMetadata, + initMigration, + updateStatus, + + startReindex, + cancelReindex, + startReadonly, + cancelReadonly, + }; +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/use_reindex_state.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/use_reindex_state.tsx deleted file mode 100644 index 2cf65a782f10b..0000000000000 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/data_streams/use_reindex_state.tsx +++ /dev/null @@ -1,284 +0,0 @@ -/* - * 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 { useRef, useCallback, useState, useEffect } from 'react'; - -import { - DataStreamReindexStatus, - DataStreamReindexWarning, - DataStreamMetadata, - DataStreamReindexStatusResponse, - DataStreamProgressDetails, -} from '../../../../../../common/types'; -import { CancelLoadingState, LoadingState } from '../../../types'; -import { ApiService } from '../../../../lib/api'; - -const POLL_INTERVAL = 1000; - -export interface ReindexState { - loadingState: LoadingState; - cancelLoadingState?: CancelLoadingState; - - status?: DataStreamReindexStatus; - reindexTaskPercComplete: number | null; - errorMessage: string | null; - reindexWarnings?: DataStreamReindexWarning[]; - hasRequiredPrivileges?: boolean; - taskStatus?: DataStreamProgressDetails; - - meta: DataStreamMetadata | null; -} - -const getReindexState = ( - reindexState: ReindexState, - { - reindexOp, - warnings, - hasRequiredPrivileges, - meta: updatedMeta, - }: DataStreamReindexStatusResponse & { meta?: DataStreamMetadata | null } -) => { - const newReindexState: ReindexState = { - ...reindexState, - - reindexWarnings: warnings, - meta: updatedMeta || reindexState.meta, - loadingState: LoadingState.Success, - }; - - if (warnings) { - newReindexState.reindexWarnings = warnings; - } - - if (hasRequiredPrivileges !== undefined) { - newReindexState.hasRequiredPrivileges = hasRequiredPrivileges; - } - - if (reindexOp) { - newReindexState.status = reindexOp.status; - - if (reindexOp.status === DataStreamReindexStatus.notStarted) { - return newReindexState; - } - - if (reindexOp.status === DataStreamReindexStatus.failed) { - newReindexState.errorMessage = reindexOp.errorMessage; - return newReindexState; - } - - if ( - reindexOp.status === DataStreamReindexStatus.inProgress || - reindexOp.status === DataStreamReindexStatus.completed - ) { - newReindexState.taskStatus = reindexOp.progressDetails; - newReindexState.reindexTaskPercComplete = reindexOp.reindexTaskPercComplete; - } - - if ( - reindexState.cancelLoadingState === CancelLoadingState.Requested && - reindexOp.status === DataStreamReindexStatus.inProgress - ) { - newReindexState.cancelLoadingState = CancelLoadingState.Loading; - } - } - - return newReindexState; -}; - -export const useReindexStatus = ({ - dataStreamName, - api, -}: { - dataStreamName: string; - api: ApiService; -}) => { - const [reindexState, setReindexState] = useState({ - loadingState: LoadingState.Loading, - errorMessage: null, - reindexTaskPercComplete: null, - taskStatus: undefined, - meta: null, - }); - - const pollIntervalIdRef = useRef | null>(null); - const isMounted = useRef(false); - - const clearPollInterval = useCallback(() => { - if (pollIntervalIdRef.current) { - clearTimeout(pollIntervalIdRef.current); - pollIntervalIdRef.current = null; - } - }, []); - - const updateStatus = useCallback(async () => { - clearPollInterval(); - try { - const { data, error } = await api.getDataStreamReindexStatus(dataStreamName); - - if (error) { - setReindexState((prevValue: ReindexState) => { - return { - ...prevValue, - loadingState: LoadingState.Error, - errorMessage: error.message.toString(), - status: DataStreamReindexStatus.fetchFailed, - }; - }); - return; - } - - if (data === null) { - return; - } - - setReindexState((prevValue: ReindexState) => { - return getReindexState(prevValue, data); - }); - - if (data.reindexOp && data.reindexOp.status === DataStreamReindexStatus.inProgress) { - // Only keep polling if it exists and is in progress. - pollIntervalIdRef.current = setTimeout(updateStatus, POLL_INTERVAL); - } - } catch (error) { - setReindexState((prevValue: ReindexState) => { - return { - ...prevValue, - loadingState: LoadingState.Error, - errorMessage: error.message.toString(), - status: DataStreamReindexStatus.fetchFailed, - }; - }); - } - }, [clearPollInterval, api, dataStreamName]); - - const startReindex = useCallback(async () => { - setReindexState((prevValue: ReindexState) => { - return { - ...prevValue, - status: DataStreamReindexStatus.inProgress, - reindexTaskPercComplete: null, - errorMessage: null, - cancelLoadingState: undefined, - }; - }); - - if (reindexState.status === DataStreamReindexStatus.failed) { - try { - await api.cancelDataStreamReindexTask(dataStreamName); - } catch (_) { - // if the task has already failed, attempt to cancel the task - // before attempting to start the reindexing again. - } - } - - const { data: reindexOp, error } = await api.startDataStreamReindexTask(dataStreamName); - - if (error) { - setReindexState((prevValue: ReindexState) => { - return { - ...prevValue, - loadingState: LoadingState.Error, - errorMessage: error.message.toString(), - status: DataStreamReindexStatus.failed, - }; - }); - return; - } - - setReindexState((prevValue: ReindexState) => { - return getReindexState(prevValue, { reindexOp, meta: prevValue.meta }); - }); - updateStatus(); - }, [api, dataStreamName, updateStatus, reindexState.status]); - - const loadDataStreamMetadata = useCallback(async () => { - try { - const { data, error } = await api.getDataStreamMetadata(dataStreamName); - - if (error) { - throw error; - } - - setReindexState((prevValue: ReindexState) => { - return { - ...prevValue, - loadingState: LoadingState.Success, - meta: data || null, - }; - }); - } catch (error) { - setReindexState((prevValue: ReindexState) => { - // if state is completed, we don't need to update the meta - if (prevValue.status === DataStreamReindexStatus.completed) { - return prevValue; - } - - return { - ...prevValue, - loadingState: LoadingState.Error, - errorMessage: error.message.toString(), - status: DataStreamReindexStatus.failed, - }; - }); - } - }, [api, dataStreamName]); - - const cancelReindex = useCallback(async () => { - setReindexState((prevValue: ReindexState) => { - return { - ...prevValue, - cancelLoadingState: CancelLoadingState.Requested, - }; - }); - try { - const { error } = await api.cancelDataStreamReindexTask(dataStreamName); - - if (error) { - throw error; - } - - setReindexState((prevValue: ReindexState) => { - return { - ...prevValue, - cancelLoadingState: CancelLoadingState.Success, - status: DataStreamReindexStatus.cancelled, - }; - }); - } catch (error) { - setReindexState((prevValue: ReindexState) => { - return { - ...prevValue, - cancelLoadingState: CancelLoadingState.Error, - }; - }); - } - }, [api, dataStreamName]); - - useEffect(() => { - updateStatus(); - }, [updateStatus]); - - useEffect(() => { - isMounted.current = true; - - return () => { - isMounted.current = false; - - // Clean up on unmount. - clearPollInterval(); - }; - }, [clearPollInterval]); - - return { - reindexState, - loadDataStreamMetadata, - - startReindex, - cancelReindex, - updateStatus, - }; -}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index.tsx index 034610d874e7d..34a09fdc93243 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index.tsx @@ -8,7 +8,7 @@ export { MlSnapshotsTableRow } from './ml_snapshots'; export { IndexSettingsTableRow } from './index_settings'; export { DefaultTableRow } from './default'; -export { ReindexTableRow } from './reindex'; +export { IndexTableRow } from './indices'; export { DataStreamTableRow } from './data_streams'; export { ClusterSettingsTableRow } from './cluster_settings'; export { HealthIndicatorTableRow } from './health_indicator'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/context.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/context.tsx new file mode 100644 index 0000000000000..a2c0a8a80cff6 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/context.tsx @@ -0,0 +1,75 @@ +/* + * 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 React, { createContext, useContext } from 'react'; + +import { ApiService } from '../../../../lib/api'; +import { useReindex, ReindexState } from './use_reindex'; +import { UpdateIndexState, useUpdateIndex } from './use_update_index'; +import { EnrichedDeprecationInfo, IndexAction } from '../../../../../../common/types'; + +export interface IndexStateContext { + deprecation: EnrichedDeprecationInfo; + reindexState: ReindexState; + startReindex: () => Promise; + cancelReindex: () => Promise; + updateIndexState: UpdateIndexState; + updateIndex: () => Promise; +} + +const IndexContext = createContext(undefined); + +export const useIndexContext = () => { + const context = useContext(IndexContext); + if (context === undefined) { + throw new Error('useIndexContext must be used within a '); + } + return context; +}; + +interface Props { + api: ApiService; + children: React.ReactNode; + deprecation: EnrichedDeprecationInfo; +} + +export const IndexStatusProvider: React.FunctionComponent = ({ + api, + deprecation, + children, +}) => { + const indexName = deprecation.index!; + const indexAction = deprecation.correctiveAction as IndexAction; + const { reindexState, startReindex, cancelReindex } = useReindex({ + indexName, + api, + isInDataStream: Boolean(indexAction?.metadata.isInDataStream), + isFrozen: Boolean(indexAction?.metadata.isFrozenIndex), + isClosedIndex: Boolean(indexAction?.metadata.isClosedIndex), + }); + + const { updateIndexState, updateIndex } = useUpdateIndex({ + indexName, + api, + correctiveAction: deprecation.correctiveAction, + }); + + return ( + + {children} + + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/container.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/container.tsx new file mode 100644 index 0000000000000..a5b7f6db16955 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/container.tsx @@ -0,0 +1,246 @@ +/* + * 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 React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFlyoutHeader, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; + +import { EnrichedDeprecationInfo, ReindexStatus } from '../../../../../../../common/types'; + +import type { IndexStateContext } from '../context'; +import { DeprecationBadge } from '../../../../shared'; +import { + UIM_REINDEX_READONLY_CLICK, + UIM_REINDEX_READONLY_RETRY_CLICK, + UIM_REINDEX_START_CLICK, + UIM_REINDEX_STOP_CLICK, + UIM_REINDEX_UNFREEZE_CLICK, + UIM_REINDEX_UNFREEZE_RETRY_CLICK, + uiMetricService, +} from '../../../../../lib/ui_metric'; +import { + ReindexDetailsFlyoutStep, + UnfreezeDetailsFlyoutStep, + UpdateIndexFlyoutStep, + ReindexFlyoutStep, + WarningFlyoutStep, + type FlyoutStep, +} from './steps'; + +export interface IndexFlyoutProps extends IndexStateContext { + deprecation: EnrichedDeprecationInfo; + closeFlyout: () => void; +} + +export const IndexFlyout: React.FunctionComponent = ({ + reindexState, + startReindex, + cancelReindex, + updateIndexState, + updateIndex, + closeFlyout, + deprecation, +}) => { + const { status: reindexStatus, reindexWarnings } = reindexState; + const { status: updateIndexStatus } = updateIndexState; + const { index, correctiveAction } = deprecation; + + const [flyoutStep, setFlyoutStep] = useState('details'); + + useEffect(() => { + switch (reindexStatus) { + case ReindexStatus.failed: + case ReindexStatus.fetchFailed: + case ReindexStatus.cancelled: + case ReindexStatus.inProgress: + case ReindexStatus.completed: { + setFlyoutStep('reindexing'); + break; + } + default: { + switch (updateIndexStatus) { + case 'inProgress': + case 'complete': + case 'failed': { + setFlyoutStep(correctiveAction?.type === 'unfreeze' ? 'unfreeze' : 'makeReadonly'); + break; + } + default: { + setFlyoutStep('details'); + break; + } + } + } + } + }, [correctiveAction?.type, reindexStatus, updateIndexStatus]); + + const onStartReindex = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_START_CLICK); + startReindex(); + }, [startReindex]); + + const onMakeReadonly = useCallback(async () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_READONLY_CLICK); + await updateIndex(); + }, [updateIndex]); + + const onMakeReadonlyRetry = useCallback(async () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_READONLY_RETRY_CLICK); + await updateIndex(); + }, [updateIndex]); + + const onUnfreeze = useCallback(async () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_UNFREEZE_CLICK); + await updateIndex(); + }, [updateIndex]); + + const onUnfreezeRetry = useCallback(async () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_UNFREEZE_RETRY_CLICK); + await updateIndex(); + }, [updateIndex]); + + const onStopReindex = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_STOP_CLICK); + cancelReindex(); + }, [cancelReindex]); + + const startReindexWithWarnings = useCallback(() => { + if ( + reindexWarnings && + reindexWarnings.length > 0 && + reindexStatus !== ReindexStatus.inProgress && + reindexStatus !== ReindexStatus.completed + ) { + setFlyoutStep('confirmReindex'); + } else { + onStartReindex(); + } + }, [reindexWarnings, reindexStatus, onStartReindex]); + + const flyoutContents = useMemo(() => { + switch (flyoutStep) { + case 'details': + return correctiveAction?.type === 'unfreeze' ? ( + { + setFlyoutStep('confirmReindex'); + }} + unfreeze={() => { + setFlyoutStep('unfreeze'); + onUnfreeze(); + }} + updateIndexState={updateIndexState} + reindexState={reindexState} + /> + ) : ( + { + setFlyoutStep('confirmReindex'); + }} + startReadonly={() => { + setFlyoutStep('confirmReadonly'); + }} + deprecation={deprecation} + updateIndexState={updateIndexState} + reindexState={reindexState} + /> + ); + case 'confirmReadonly': + case 'confirmReindex': + const flow = flyoutStep === 'confirmReadonly' ? 'readonly' : 'reindex'; + return ( + warningFlow === 'all' || warningFlow === flow + ) ?? [] + } + meta={reindexState.meta} + flow={flow} + back={() => setFlyoutStep('details')} + confirm={() => { + if (flyoutStep === 'confirmReadonly') { + setFlyoutStep('makeReadonly'); + onMakeReadonly(); + } else { + onStartReindex(); + } + }} + /> + ); + case 'reindexing': + return ( + + ); + case 'unfreeze': + return ( + + ); + case 'makeReadonly': + return ( + + ); + } + }, [ + flyoutStep, + correctiveAction?.type, + deprecation, + closeFlyout, + updateIndexState, + reindexState, + startReindexWithWarnings, + onStopReindex, + onUnfreezeRetry, + onMakeReadonlyRetry, + onUnfreeze, + onMakeReadonly, + onStartReindex, + ]); + + return ( + <> + + + + +

    + +

    + + + + {flyoutContents} + + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/index.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/index.tsx similarity index 71% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/index.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/index.tsx index dad239ddb81b3..87ae80e7f0708 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/index.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/index.tsx @@ -5,5 +5,5 @@ * 2.0. */ -export type { ReindexFlyoutProps } from './container'; -export { ReindexFlyout } from './container'; +export type { IndexFlyoutProps } from './container'; +export { IndexFlyout } from './container'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/es_transform_target_guidance.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/es_transform_target_guidance.tsx new file mode 100644 index 0000000000000..7a674d6d0bc95 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/es_transform_target_guidance.tsx @@ -0,0 +1,79 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { EnrichedDeprecationInfo } from '../../../../../../../../../common/types'; +import { useAppContext } from '../../../../../../../app_context'; + +interface Props { + deprecation: EnrichedDeprecationInfo; +} + +/** + * We get copy directly from ES. This contains information that applies to indices + * that are read-only or not. + */ +export const ESTransformsTargetGuidance = ({ deprecation }: Props) => { + const { + services: { + core: { http }, + }, + } = useAppContext(); + return ( + <> + + {deprecation.details} + + + +

    + +

    +

    + + {i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.esTransform.migrationGuideLink', + { defaultMessage: 'migration guide' } + )} + + ), + transformsLink: ( + + + + ), + }} + /> +

    +
    + + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/index.ts b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/index.ts new file mode 100644 index 0000000000000..c5880db9832f7 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { ReindexDetailsFlyoutStep } from './reindex_details_step'; +export { UnfreezeDetailsFlyoutStep } from './unfreeze_details_step'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/messages.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/messages.tsx new file mode 100644 index 0000000000000..644162b7c9918 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/messages.tsx @@ -0,0 +1,151 @@ +/* + * 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 React, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink, EuiSpacer } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ReindexStatus } from '../../../../../../../../../common/types'; +import { IndexClosedParagraph } from '../index_closed_paragraph'; + +export const getReindexButtonLabel = (status?: ReindexStatus) => { + switch (status) { + case ReindexStatus.fetchFailed: + case ReindexStatus.failed: + return ( + + ); + case ReindexStatus.inProgress: + return ( + + ); + case ReindexStatus.cancelled: + return ( + + ); + default: + return ( + + ); + } +}; + +export const getDefaultGuideanceText = ({ + isClosedIndex, + readOnlyExcluded, + reindexExcluded, + indexBlockUrl, + indexManagementUrl, +}: { + isClosedIndex: boolean; + readOnlyExcluded: boolean; + reindexExcluded: boolean; + indexBlockUrl: string; + indexManagementUrl: string; +}) => { + const guideanceListItems = []; + if (!reindexExcluded) { + guideanceListItems.push({ + title: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option1.title', + { + defaultMessage: 'Option {optionCount}: Reindex data', + values: { optionCount: guideanceListItems.length + 1 }, + } + ), + description: ( + + + {isClosedIndex && ( + + + + + )} + + ), + }); + } + + if (!readOnlyExcluded) { + guideanceListItems.push({ + title: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option2.title', + { + defaultMessage: 'Option {optionCount}: Mark as read-only', + values: { optionCount: guideanceListItems.length + 1 }, + } + ), + description: ( + + + {i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel', + { + defaultMessage: 'Learn more', + } + )} + + ), + }} + /> + + ), + }); + } + + guideanceListItems.push({ + title: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option3.title', + { + defaultMessage: 'Option {optionCount}: Delete index', + values: { optionCount: guideanceListItems.length + 1 }, + } + ), + description: ( + + + + + ), + }} + /> + + ), + }); + + return guideanceListItems; +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/ml_anomaly_guidance.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/ml_anomaly_guidance.tsx new file mode 100644 index 0000000000000..900059b62b3e4 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/ml_anomaly_guidance.tsx @@ -0,0 +1,98 @@ +/* + * 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 type { FunctionComponent } from 'react'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut, EuiDescriptionList, EuiLink, EuiText } from '@elastic/eui'; +import { useAppContext } from '../../../../../../../app_context'; + +export const MlAnomalyGuidance: FunctionComponent = () => { + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + return ( + <> +

    + + + {i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.notCompatibleMlAnomalyIndexText.learnMore', + { defaultMessage: 'Learn more' } + )} + + ), + }} + /> + +

    + + +
    + ), + }, + { + title: 'Option 2: Mark as read-only', + description: ( + + + {i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel', + { + defaultMessage: 'Learn more', + } + )} + + ), + }} + /> + + ), + }, + { + title: 'Option 3: Delete this index', + description: ( + + + + ), + }, + ]} + /> + + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/reindex_details_step.test.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/reindex_details_step.test.tsx new file mode 100644 index 0000000000000..7ee168ea6fd08 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/reindex_details_step.test.tsx @@ -0,0 +1,493 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; +import { ReindexDetailsFlyoutStep } from './reindex_details_step'; +import type { ReindexState } from '../../../use_reindex'; +import type { UpdateIndexState } from '../../../use_update_index'; +import { LoadingState } from '../../../../../../types'; +import { EnrichedDeprecationInfo } from '../../../../../../../../../common/types'; + +jest.mock('../../../../../../../app_context', () => { + const { docLinksServiceMock } = jest.requireActual('@kbn/core-doc-links-browser-mocks'); + + return { + useAppContext: () => { + return { + services: { + api: { + useLoadNodeDiskSpace: () => [], + }, + core: { + docLinks: docLinksServiceMock.createStartContract(), + http: { + basePath: { + prepend: jest.fn(), + }, + }, + }, + }, + }; + }, + }; +}); + +describe('ReindexDetailsFlyoutStep', () => { + const defaultDeprecation: () => EnrichedDeprecationInfo = () => ({ + isCritical: true, + message: 'foo', + resolveDuringUpgrade: false, + type: 'index_settings', + url: 'https://te.st', + }); + const defaultReindexState: () => ReindexState = () => ({ + loadingState: LoadingState.Success, + meta: { + indexName: 'some_index', + reindexName: 'some_index-reindexed-for-9', + aliases: [], + isInDataStream: false, + isFrozen: false, + isReadonly: false, + isClosedIndex: false, + }, + hasRequiredPrivileges: true, + reindexTaskPercComplete: null, + errorMessage: null, + }); + + const defaultUpdateIndexState: () => UpdateIndexState = () => ({ + status: 'incomplete', + failedBefore: false, + }); + + it('renders for non-readonly indices', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + +

    + +

    + + +
    , + "title": "Option 1: Reindex data", + }, + Object { + "description": + + Learn more + , + } + } + /> + , + "title": "Option 2: Mark as read-only", + }, + Object { + "description": + + + , + } + } + /> + , + "title": "Option 3: Delete index", + }, + ] + } + rowGutterSize="m" + /> + + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + `); + }); + + it('renders correct guidance for indices with transforms', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchInlineSnapshot(` + + + + + + + + + + + + + + + + + + + + + + + + + + + `); + }); + + it('renders for readonly indices (warning deprecation)', () => { + const props = defaultReindexState(); + props.meta.isReadonly = true; + + const wrapper = shallow( + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + +

    + +

    +

    + +

    +
    + +
    + + + + + + + + + + + + + + + + + + +
    + `); + }); + + it('renders ML anomaly index guidance', () => { + const reindexState = defaultReindexState(); + reindexState.meta.indexName = '.ml-anomalies-1'; + const deprecation = defaultDeprecation(); + deprecation.index = '.ml-anomalies-1'; + const wrapper = shallow( + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `); + }); +}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/reindex_details_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/reindex_details_step.tsx new file mode 100644 index 0000000000000..ebe07b262b7af --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/reindex_details_step.tsx @@ -0,0 +1,270 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + EnrichedDeprecationInfo, + ReindexAction, + ReindexStatus, +} from '../../../../../../../../../common/types'; +import { LoadingState } from '../../../../../../types'; +import type { ReindexState } from '../../../use_reindex'; +import { useAppContext } from '../../../../../../../app_context'; +import { getDefaultGuideanceText, getReindexButtonLabel } from './messages'; +import { FrozenCallOut } from '../frozen_callout'; +import type { UpdateIndexState } from '../../../use_update_index'; +import { FetchFailedCallOut } from '../fetch_failed_callout'; +import { ReindexingFailedCallOut } from '../reindexing_failed_callout'; +import { MlAnomalyGuidance } from './ml_anomaly_guidance'; +import { ESTransformsTargetGuidance } from './es_transform_target_guidance'; +import { IndexClosedParagraph } from '../index_closed_paragraph'; + +const ML_ANOMALIES_PREFIX = '.ml-anomalies-'; + +/** + * Displays a flyout that shows the details / corrective action for a "reindex" deprecation for a given index. + */ +export const ReindexDetailsFlyoutStep: React.FunctionComponent<{ + reindexState: ReindexState; + updateIndexState: UpdateIndexState; + deprecation: EnrichedDeprecationInfo; + startReindex: () => void; + startReadonly: () => void; + closeFlyout: () => void; +}> = ({ + reindexState, + updateIndexState, + deprecation, + startReindex, + startReadonly, + closeFlyout, +}) => { + const { + services: { + api, + core: { docLinks, http }, + }, + } = useAppContext(); + + const { loadingState, status: reindexStatus, hasRequiredPrivileges, meta } = reindexState; + const { status: updateIndexStatus } = updateIndexState; + const { indexName, isFrozen, isClosedIndex, isReadonly } = meta; + const loading = loadingState === LoadingState.Loading; + const isCompleted = reindexStatus === ReindexStatus.completed || updateIndexStatus === 'complete'; + const hasFetchFailed = reindexStatus === ReindexStatus.fetchFailed; + const hasReindexingFailed = reindexStatus === ReindexStatus.failed; + const correctiveAction = deprecation.correctiveAction as ReindexAction | undefined; + const isESTransformTarget = !!correctiveAction?.transformIds?.length; + const isMLAnomalyIndex = Boolean(indexName?.startsWith(ML_ANOMALIES_PREFIX)); + const { excludedActions = [] } = (deprecation.correctiveAction as ReindexAction) || {}; + const readOnlyExcluded = excludedActions.includes('readOnly'); + const reindexExcluded = excludedActions.includes('reindex'); + + const { data: nodes } = api.useLoadNodeDiskSpace(); + + let showEsTransformsGuidance = false; + let showMlAnomalyReindexingGuidance = false; + let showReadOnlyGuidance = false; + let showDefaultGuidance = false; + + if (isESTransformTarget) { + showEsTransformsGuidance = true; + } else if (isReadonly) { + showReadOnlyGuidance = true; + } else if (isMLAnomalyIndex) { + showMlAnomalyReindexingGuidance = true; + } else { + showDefaultGuidance = true; + } + + return ( + + + {hasRequiredPrivileges === false && ( + + + + } + color="danger" + iconType="warning" + /> + + )} + + {nodes && nodes.length > 0 && ( + <> + + } + > + <> + + + + +
      + {nodes.map(({ nodeName, available, nodeId }) => ( +
    • + +
    • + ))} +
    + +
    + + + )} + + {hasFetchFailed && } + + {!hasFetchFailed && hasReindexingFailed && ( + + )} + + {isFrozen && } + + + {showEsTransformsGuidance && } + {showMlAnomalyReindexingGuidance && } + {showReadOnlyGuidance && ( + +

    + +

    +

    + +

    + {isClosedIndex && ( +

    + +

    + )} +
    + )} + {showDefaultGuidance && ( + +

    + +

    + +
    + )} +
    + +
    + + + + + + + + + + {!isReadonly && + !hasFetchFailed && + !isCompleted && + hasRequiredPrivileges && + !isESTransformTarget && + !readOnlyExcluded && ( + + + + + + )} + {!hasFetchFailed && !isCompleted && hasRequiredPrivileges && !reindexExcluded && ( + + + {getReindexButtonLabel(reindexStatus)} + + + )} + + + + +
    + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/unfreeze_details_step.test.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/unfreeze_details_step.test.tsx new file mode 100644 index 0000000000000..952088b5dee0c --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/unfreeze_details_step.test.tsx @@ -0,0 +1,98 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; +import type { ReindexState } from '../../../use_reindex'; +import type { UpdateIndexState } from '../../../use_update_index'; +import { LoadingState } from '../../../../../../types'; +import { UnfreezeDetailsFlyoutStep } from './unfreeze_details_step'; + +jest.mock('../../../../../../../app_context', () => { + const { docLinksServiceMock } = jest.requireActual('@kbn/core-doc-links-browser-mocks'); + + return { + useAppContext: () => { + return { + services: { + api: { + useLoadNodeDiskSpace: () => [], + }, + core: { + docLinks: docLinksServiceMock.createStartContract(), + http: { + basePath: { + prepend: jest.fn(), + }, + }, + }, + }, + }; + }, + }; +}); + +describe('UnfreezeDetailsFlyoutStep', () => { + const defaultReindexState: ReindexState = { + loadingState: LoadingState.Success, + meta: { + indexName: 'some_index', + aliases: [], + isFrozen: true, + isReadonly: true, + isInDataStream: false, + isClosedIndex: false, + reindexName: 'some_index-reindexed-for-9', + }, + hasRequiredPrivileges: true, + reindexTaskPercComplete: null, + errorMessage: null, + }; + + const defaultUpdateIndexState: UpdateIndexState = { + status: 'incomplete', + failedBefore: false, + }; + + it('renders all options for regular indices', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('EuiButton[data-test-subj="startReindexingButton"]')).toHaveLength(1); + expect(wrapper.find('EuiButton[data-test-subj="startUnfreezeButton"]')).toHaveLength(1); + }); + + it('does NOT render Reindex option for data stream backing indices', () => { + const backingIndexReindexState = { + ...defaultReindexState, + meta: { + ...defaultReindexState.meta, + isInDataStream: true, + }, + }; + + const wrapper = shallow( + + ); + + expect(wrapper.find('EuiButton[data-test-subj="startReindexingButton"]')).toHaveLength(0); + expect(wrapper.find('EuiButton[data-test-subj="startUnfreezeButton"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/unfreeze_details_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/unfreeze_details_step.tsx new file mode 100644 index 0000000000000..9b8af7d52b13d --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/details/unfreeze_details_step.tsx @@ -0,0 +1,265 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { ReindexStatus } from '../../../../../../../../../common/types'; +import { LoadingState } from '../../../../../../types'; +import type { ReindexState } from '../../../use_reindex'; +import { useAppContext } from '../../../../../../../app_context'; +import { getReindexButtonLabel } from './messages'; +import type { UpdateIndexState } from '../../../use_update_index'; +import { FetchFailedCallOut } from '../fetch_failed_callout'; +import { ReindexingFailedCallOut } from '../reindexing_failed_callout'; +import { IndexClosedParagraph } from '../index_closed_paragraph'; + +/** + * Displays a flyout that shows the details / corrective action for a "reindex" deprecation for a given index. + */ +export const UnfreezeDetailsFlyoutStep: React.FunctionComponent<{ + closeFlyout: () => void; + reindexState: ReindexState; + updateIndexState: UpdateIndexState; + startReindex: () => void; + unfreeze: () => void; +}> = ({ closeFlyout, reindexState, updateIndexState, startReindex, unfreeze }) => { + const { + services: { + api, + core: { http }, + }, + } = useAppContext(); + + const { loadingState, status: reindexStatus, hasRequiredPrivileges, meta } = reindexState; + const { status: updateIndexStatus } = updateIndexState; + const { indexName, isInDataStream, isClosedIndex } = meta; + const loading = loadingState === LoadingState.Loading; + const isCompleted = reindexStatus === ReindexStatus.completed || updateIndexStatus === 'complete'; + const hasFetchFailed = reindexStatus === ReindexStatus.fetchFailed; + const hasReindexingFailed = reindexStatus === ReindexStatus.failed; + + const { data: nodes } = api.useLoadNodeDiskSpace(); + + return ( + + + {hasRequiredPrivileges === false && ( + + + + } + color="danger" + iconType="warning" + /> + + )} + + {nodes && nodes.length > 0 && ( + <> + + } + > + <> + + + + +
      + {nodes.map(({ nodeName, available, nodeId }) => ( +
    • + +
    • + ))} +
    + +
    + + + )} + + {hasFetchFailed && } + + {!hasFetchFailed && hasReindexingFailed && ( + + )} + + +

    + +

    + + +
    + ), + }, + /* We cannot reindex backing indices in the same way as regular indices (that would break the related data_stream) */ + ...(!isInDataStream + ? [ + { + title: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.unfreeze.option2.title', + { + defaultMessage: 'Option 2: Reindex data', + } + ), + description: ( + + + {isClosedIndex && ( + + + + + )} + + ), + }, + ] + : []), + { + title: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.unfreeze.alternativeOption.title', + { + defaultMessage: 'Alternative: Delete the index', + } + ), + description: ( + + + + + ), + }} + /> + + ), + }, + ]} + /> + + +
    + + + + + + + + + + {/* We cannot reindex backing indices in the same way as regular indices (that would break the related data_stream) */} + {!isInDataStream && !hasFetchFailed && !isCompleted && hasRequiredPrivileges && ( + + + {getReindexButtonLabel(reindexStatus)} + + + )} + {!hasFetchFailed && !isCompleted && hasRequiredPrivileges && ( + + + + + + )} + + + + +
    + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/fetch_failed_callout.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/fetch_failed_callout.tsx new file mode 100644 index 0000000000000..36fea18acd5b5 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/fetch_failed_callout.tsx @@ -0,0 +1,36 @@ +/* + * 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 React, { Fragment } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + errorMessage: string; +} + +export const FetchFailedCallOut: React.FunctionComponent = (props) => { + const { errorMessage } = props; + return ( + + + } + > + {errorMessage} + + + + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/frozen_callout.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/frozen_callout.tsx new file mode 100644 index 0000000000000..bc7b5ae96be46 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/frozen_callout.tsx @@ -0,0 +1,52 @@ +/* + * 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 React, { Fragment } from 'react'; +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { useAppContext } from '../../../../../../app_context'; + +export const FrozenCallOut: React.FunctionComponent = () => { + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + + return ( + + + } + iconType="iInCircle" + > + + {i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel', + { + defaultMessage: 'Learn more', + } + )} + + ), + }} + /> + + + + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/index.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/index.tsx new file mode 100644 index 0000000000000..ab9e6f5ae1400 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/index.tsx @@ -0,0 +1,13 @@ +/* + * 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 type { FlyoutStep } from './types'; +export { ReindexDetailsFlyoutStep } from './details/reindex_details_step'; +export { UnfreezeDetailsFlyoutStep } from './details/unfreeze_details_step'; +export { WarningFlyoutStep } from './warning/warning_step'; +export { ReindexFlyoutStep } from './reindex/reindex_step'; +export { UpdateIndexFlyoutStep } from './update/update_step'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/index_closed_paragraph.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/index_closed_paragraph.tsx new file mode 100644 index 0000000000000..d3ab45166fa88 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/index_closed_paragraph.tsx @@ -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 React from 'react'; +import { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { useAppContext } from '../../../../../../app_context'; + +export const IndexClosedParagraph: React.FunctionComponent = () => { + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + + return ( + + {i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel', + { + defaultMessage: 'Learn more', + } + )} + + ), + reindexingMayTakeLongerEmph: ( + + {i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexTakesLonger', + { defaultMessage: 'Reindexing may take longer than usual' } + )} + + ), + }} + /> + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/__snapshots__/reindex_step.test.tsx.snap similarity index 71% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/__snapshots__/reindex_step.test.tsx.snap index f790b5173ce61..14c46f0bc8260 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/__snapshots__/reindex_step.test.tsx.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ChecklistFlyout renders 1`] = ` +exports[`ReindexStep renders 1`] = `

    @@ -39,6 +39,10 @@ exports[`ChecklistFlyout renders 1`] = ` "meta": Object { "aliases": Array [], "indexName": "myIndex", + "isClosedIndex": false, + "isFrozen": false, + "isInDataStream": false, + "isReadonly": false, "reindexName": "reindexed-myIndex", }, "reindexTaskPercComplete": null, @@ -62,7 +66,7 @@ exports[`ChecklistFlyout renders 1`] = ` > @@ -79,7 +83,7 @@ exports[`ChecklistFlyout renders 1`] = ` > @@ -88,14 +92,15 @@ exports[`ChecklistFlyout renders 1`] = `
    `; -exports[`ChecklistFlyout renders for frozen indices 1`] = ` +exports[`ReindexStep renders for frozen indices 1`] = ` +

    - - } - > - - Learn more - , - } - } - /> - -

    @@ -152,6 +132,10 @@ exports[`ChecklistFlyout renders for frozen indices 1`] = ` "meta": Object { "aliases": Array [], "indexName": "myIndex", + "isClosedIndex": false, + "isFrozen": true, + "isInDataStream": false, + "isReadonly": false, "reindexName": "reindexed-myIndex", }, "reindexTaskPercComplete": null, @@ -175,7 +159,7 @@ exports[`ChecklistFlyout renders for frozen indices 1`] = ` > @@ -192,7 +176,7 @@ exports[`ChecklistFlyout renders for frozen indices 1`] = ` > diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/progress.test.tsx similarity index 80% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/progress.test.tsx index 37137d46f2285..67442bdd1970b 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/progress.test.tsx @@ -8,9 +8,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; -import { LoadingState } from '../../../../types'; -import type { ReindexState } from '../use_reindex_state'; +import { ReindexStatus, ReindexStep } from '../../../../../../../../../common/types'; +import { LoadingState } from '../../../../../../types'; +import type { ReindexState } from '../../../use_reindex'; import { ReindexProgress } from './progress'; describe('ReindexProgress', () => { @@ -28,6 +28,10 @@ describe('ReindexProgress', () => { indexName: 'foo', reindexName: 'reindexed-foo', aliases: [], + isFrozen: false, + isReadonly: false, + isInDataStream: false, + isClosedIndex: false, }, } as ReindexState } @@ -44,7 +48,7 @@ describe('ReindexProgress', () => {

    { "status": "inProgress", "title": @@ -74,7 +78,7 @@ describe('ReindexProgress', () => { "status": "incomplete", "title": @@ -96,6 +100,10 @@ describe('ReindexProgress', () => { "meta": Object { "aliases": Array [], "indexName": "foo", + "isClosedIndex": false, + "isFrozen": false, + "isInDataStream": false, + "isReadonly": false, "reindexName": "reindexed-foo", }, "reindexTaskPercComplete": null, @@ -108,7 +116,7 @@ describe('ReindexProgress', () => { "status": "incomplete", "title": @@ -125,7 +133,7 @@ describe('ReindexProgress', () => { "status": "incomplete", "title": @@ -142,7 +150,7 @@ describe('ReindexProgress', () => { "status": "incomplete", "title": @@ -173,6 +181,10 @@ describe('ReindexProgress', () => { indexName: 'foo', reindexName: 'reindexed-foo', aliases: [], + isFrozen: true, + isReadonly: false, + isInDataStream: false, + isClosedIndex: false, }, } as ReindexState } diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/progress.tsx similarity index 78% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/progress.tsx index 8457225c43fa6..6a72672edbcf7 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/progress.tsx @@ -18,11 +18,11 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; -import { CancelLoadingState } from '../../../../types'; -import type { ReindexState } from '../use_reindex_state'; -import { StepProgress, StepProgressStep } from './step_progress'; -import { getReindexProgressLabel } from '../../../../../lib/utils'; +import { ReindexStatus, ReindexStep } from '../../../../../../../../../common/types'; +import { CancelLoadingState } from '../../../../../../types'; +import type { ReindexState } from '../../../use_reindex'; +import { StepProgress, StepProgressStep } from '../../../../../common/step_progress'; +import { getReindexProgressLabel } from '../../../../../../../lib/utils'; const ErrorCallout: React.FunctionComponent<{ errorMessage: string | null }> = ({ errorMessage, @@ -49,7 +49,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{ return ( <> @@ -71,7 +71,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{ case CancelLoadingState.Loading: cancelText = ( ); @@ -79,7 +79,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{ case CancelLoadingState.Success: cancelText = ( ); @@ -87,7 +87,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{ case CancelLoadingState.Error: cancelText = ( ); @@ -95,7 +95,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{ default: cancelText = ( ); @@ -106,12 +106,12 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{ {stepInProgress ? ( ) : ( )} @@ -141,7 +141,7 @@ const getStepTitle = ( if (step === ReindexStep.readonly) { return inProgress ? ( {meta.indexName}, @@ -149,7 +149,7 @@ const getStepTitle = ( /> ) : ( {meta.indexName}, @@ -161,7 +161,7 @@ const getStepTitle = ( if (step === ReindexStep.newIndexCreated) { return inProgress ? ( {meta.reindexName}, @@ -169,7 +169,7 @@ const getStepTitle = ( /> ) : ( {meta.reindexName}, @@ -181,7 +181,7 @@ const getStepTitle = ( if (step === ReindexStep.indexSettingsRestored) { return inProgress ? ( {meta.indexName}, @@ -190,7 +190,7 @@ const getStepTitle = ( /> ) : ( {meta.indexName}, @@ -203,7 +203,7 @@ const getStepTitle = ( if (step === ReindexStep.aliasCreated) { return inProgress ? ( {meta.indexName}, @@ -212,7 +212,7 @@ const getStepTitle = ( /> ) : ( {meta.indexName}, @@ -225,7 +225,7 @@ const getStepTitle = ( if (step === ReindexStep.originalIndexDeleted) { return inProgress ? ( {meta.indexName}, @@ -233,7 +233,7 @@ const getStepTitle = ( /> ) : ( {meta.indexName}, @@ -245,7 +245,7 @@ const getStepTitle = ( if (step === ReindexStep.existingAliasesUpdated) { return inProgress ? ( {`[${meta.aliases.join(',')}]`}, @@ -254,7 +254,7 @@ const getStepTitle = ( /> ) : ( {`[${meta.aliases.join(',')}]`}, @@ -378,7 +378,7 @@ export const ReindexProgress: React.FunctionComponent = (props) => {

    {status === ReindexStatus.inProgress ? ( = (props) => { /> ) : ( )} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/reindex_step.test.tsx similarity index 66% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/reindex_step.test.tsx index a895f5fad815d..a4a18c29512e9 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/reindex_step.test.tsx @@ -9,12 +9,12 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; -import { ReindexStatus } from '../../../../../../../common/types'; -import { LoadingState } from '../../../../types'; -import type { ReindexState } from '../use_reindex_state'; -import { ChecklistFlyoutStep } from './checklist_step'; +import { ReindexStatus } from '../../../../../../../../../common/types'; +import { LoadingState } from '../../../../../../types'; +import type { ReindexState } from '../../../use_reindex'; +import { ReindexFlyoutStep } from './reindex_step'; -jest.mock('../../../../../app_context', () => { +jest.mock('../../../../../../../app_context', () => { const { docLinksServiceMock } = jest.requireActual('@kbn/core-doc-links-browser-mocks'); return { @@ -33,10 +33,9 @@ jest.mock('../../../../../app_context', () => { }; }); -describe('ChecklistFlyout', () => { +describe('ReindexStep', () => { const defaultProps = { indexName: 'myIndex', - frozen: false, closeFlyout: jest.fn(), confirmInputValue: 'CONFIRM', onConfirmInputChange: jest.fn(), @@ -60,31 +59,35 @@ describe('ChecklistFlyout', () => { indexName: 'myIndex', reindexName: 'reindexed-myIndex', aliases: [], + isReadonly: false, + isFrozen: false, + isInDataStream: false, + isClosedIndex: false, }, } as ReindexState, }; it('renders', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); it('renders for frozen indices', () => { const props = cloneDeep(defaultProps); - props.frozen = true; - expect(shallow()).toMatchSnapshot(); + props.reindexState.meta.isFrozen = true; + expect(shallow()).toMatchSnapshot(); }); it('disables button while reindexing', () => { const props = cloneDeep(defaultProps); props.reindexState.status = ReindexStatus.inProgress; - const wrapper = shallow(); + const wrapper = shallow(); expect((wrapper.find('EuiButton').props() as any).isLoading).toBe(true); }); it('hides button if hasRequiredPrivileges is false', () => { const props = cloneDeep(defaultProps); props.reindexState.hasRequiredPrivileges = false; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.exists('EuiButton')).toBe(false); }); @@ -92,24 +95,30 @@ describe('ChecklistFlyout', () => { const props = cloneDeep(defaultProps); props.reindexState.status = ReindexStatus.fetchFailed; props.reindexState.errorMessage = 'Index not found'; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.exists('EuiButton')).toBe(false); }); - it('shows get status error callout', () => { + it('shows fetch failed error callout', () => { const props = cloneDeep(defaultProps); props.reindexState.status = ReindexStatus.fetchFailed; props.reindexState.errorMessage = 'Index not found'; - const wrapper = shallow(); - expect(wrapper.exists('[data-test-subj="fetchFailedCallout"]')).toBe(true); + const wrapper = shallow(); + expect(wrapper.find('FetchFailedCallOut').exists()).toBe(true); + expect(wrapper.find('FetchFailedCallOut').props()).toEqual({ + errorMessage: 'Index not found', + }); }); it('shows reindexing callout', () => { const props = cloneDeep(defaultProps); props.reindexState.status = ReindexStatus.failed; - props.reindexState.errorMessage = 'Index not found'; - const wrapper = shallow(); - expect(wrapper.exists('[data-test-subj="reindexingFailedCallout"]')).toBe(true); + props.reindexState.errorMessage = 'Reindex failed'; + const wrapper = shallow(); + expect(wrapper.find('ReindexingFailedCallOut').exists()).toBe(true); + expect(wrapper.find('ReindexingFailedCallOut').props()).toEqual({ + errorMessage: 'Reindex failed', + }); }); it('calls startReindex when button is clicked', () => { @@ -121,7 +130,7 @@ describe('ChecklistFlyout', () => { status: undefined, }, }; - const wrapper = shallow(); + const wrapper = shallow(); wrapper.find('EuiButton').simulate('click'); expect(props.startReindex).toHaveBeenCalled(); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/reindex_step.tsx similarity index 58% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/reindex_step.tsx index 0710d560d7d93..e0e9c44a8eea2 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindex/reindex_step.tsx @@ -22,46 +22,49 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { ReindexStatus } from '../../../../../../../common/types'; -import { LoadingState } from '../../../../types'; -import type { ReindexState } from '../use_reindex_state'; +import { ReindexStatus } from '../../../../../../../../../common/types'; +import { LoadingState } from '../../../../../../types'; +import type { ReindexState } from '../../../use_reindex'; import { ReindexProgress } from './progress'; -import { useAppContext } from '../../../../../app_context'; +import { useAppContext } from '../../../../../../../app_context'; +import { FrozenCallOut } from '../frozen_callout'; +import { FetchFailedCallOut } from '../fetch_failed_callout'; +import { ReindexingFailedCallOut } from '../reindexing_failed_callout'; const buttonLabel = (status?: ReindexStatus) => { switch (status) { case ReindexStatus.failed: return ( ); case ReindexStatus.inProgress: return ( ); case ReindexStatus.paused: return ( ); case ReindexStatus.cancelled: return ( ); default: return ( ); @@ -71,13 +74,12 @@ const buttonLabel = (status?: ReindexStatus) => { /** * Displays a flyout that shows the current reindexing status for a given index. */ -export const ChecklistFlyoutStep: React.FunctionComponent<{ - frozen?: boolean; +export const ReindexFlyoutStep: React.FunctionComponent<{ closeFlyout: () => void; reindexState: ReindexState; startReindex: () => void; cancelReindex: () => void; -}> = ({ frozen, closeFlyout, reindexState, startReindex, cancelReindex }) => { +}> = ({ closeFlyout, reindexState, startReindex, cancelReindex }) => { const { services: { api, @@ -96,13 +98,14 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ return ( + {reindexState.meta.isFrozen && } {hasRequiredPrivileges === false && ( } @@ -111,7 +114,6 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ /> )} - {nodes && nodes.length > 0 && ( <> } > <> @@ -137,7 +139,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ {nodes.map(({ nodeName, available, nodeId }) => (
  • )} - - {(hasFetchFailed || hasReindexingFailed) && ( - <> - - ) : ( - - ) - } - > - {reindexState.errorMessage} - - - + {hasFetchFailed && } + {!hasFetchFailed && hasReindexingFailed && ( + )} -

    {i18n.translate( - 'xpack.upgradeAssistant.checkupTab.reindexing.flyout.learnMoreLinkLabel', + 'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel', { defaultMessage: 'Learn more', } @@ -198,40 +177,9 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ }} />

    - {frozen && ( - <> - - } - iconType="iInCircle" - > - - {i18n.translate( - 'xpack.upgradeAssistant.checkupTab.reindexing.flyout.learnMoreLinkLabel', - { - defaultMessage: 'Learn more', - } - )} - - ), - }} - /> - - - - )}

    @@ -244,7 +192,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindexing_failed_callout.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindexing_failed_callout.tsx new file mode 100644 index 0000000000000..4b713575c1907 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/reindexing_failed_callout.tsx @@ -0,0 +1,36 @@ +/* + * 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 React, { Fragment } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + errorMessage: string; +} + +export const ReindexingFailedCallOut: React.FunctionComponent = (props) => { + const { errorMessage } = props; + return ( + + + } + > + {errorMessage} + + + + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/types.ts b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/types.ts new file mode 100644 index 0000000000000..43a59deda42bf --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/types.ts @@ -0,0 +1,15 @@ +/* + * 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 type FlyoutStep = + | 'details' + | 'confirmReadonly' + | 'confirmReindex' + | 'reindexing' + | 'makeReadonly' + | 'unfreeze' + | 'completed'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/update/update_step.test.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/update/update_step.test.tsx new file mode 100644 index 0000000000000..c663b15278c00 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/update/update_step.test.tsx @@ -0,0 +1,169 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; +import { UpdateIndexFlyoutStep } from './update_step'; +import type { ReindexState } from '../../../use_reindex'; +import type { UpdateIndexState } from '../../../use_update_index'; + +describe('UpdateIndexFlyoutStep', () => { + const meta: ReindexState['meta'] = { + indexName: 'some_index', + aliases: [], + isInDataStream: false, + isFrozen: false, + isReadonly: false, + isClosedIndex: false, + reindexName: 'some_index-reindexed-for-9', + }; + + const defaultUpdateIndexState: UpdateIndexState = { + status: 'incomplete', + failedBefore: false, + }; + + it('renders makeReadonly operation', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + +

    + +

    +
    + + + some_index + , + } + } + />, + }, + ] + } + /> +
    + + + + + + + + + +
    + `); + }); + + it('renders unfreeze operation', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + +

    + +

    +
    + + + some_index + , + } + } + />, + }, + ] + } + /> +
    + + + + + + + + + +
    + `); + }); +}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/update/update_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/update/update_step.tsx new file mode 100644 index 0000000000000..4e4bcfca8a359 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/update/update_step.tsx @@ -0,0 +1,143 @@ +/* + * 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 React, { Fragment } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCode, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { UpdateIndexState } from '../../../use_update_index'; +import { FrozenCallOut } from '../frozen_callout'; +import { StepProgress, type StepProgressStep } from '../../../../../common/step_progress'; +import type { ReindexState } from '../../../use_reindex'; + +interface UpdateIndexFlyoutStepProps { + action: 'unfreeze' | 'makeReadonly'; + closeFlyout: () => void; + meta: ReindexState['meta']; + updateIndexState: UpdateIndexState; + retry: () => void; +} + +const ErrorCallout: React.FunctionComponent<{ reason: string }> = ({ reason }) => ( + + +

    {reason}

    +
    +
    +); + +/** + * In charge of rendering the result of the make read-only calls + */ +export const UpdateIndexFlyoutStep: React.FunctionComponent = ({ + action, + closeFlyout, + meta, + updateIndexState, + retry, +}) => { + const { isFrozen, indexName } = meta; + const { status, failedBefore, reason } = updateIndexState; + const title = + action === 'makeReadonly' ? ( + {indexName}, + }} + /> + ) : ( + {indexName}, + }} + /> + ); + + const steps: StepProgressStep[] = [ + { + title, + status, + ...(reason && { children: }), + }, + ]; + + return ( + + + {isFrozen && } + +

    + {(status === 'inProgress' || status === 'incomplete') && ( + + )} + {status === 'complete' && ( + + )} + {status === 'failed' && ( + + )} +

    +
    + + +
    + + + + + + + + {status !== 'complete' && failedBefore && ( + + + + + + )} + + +
    + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/__snapshots__/warning_step.test.tsx.snap similarity index 73% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/__snapshots__/warning_step.test.tsx.snap index 8c45baf390461..3fb592c0d707d 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/__snapshots__/warning_step.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`WarningsFlyoutStep renders 1`] = ` +exports[`WarningFlyoutStep renders 1`] = ` @@ -17,7 +17,7 @@ exports[`WarningsFlyoutStep renders 1`] = ` >
    @@ -26,13 +26,14 @@ exports[`WarningsFlyoutStep renders 1`] = ` > diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/warning_step.test.tsx similarity index 67% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/warning_step.test.tsx index 7c6f7d554082a..821c4a201f050 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/warning_step.test.tsx @@ -10,12 +10,12 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import SemVer from 'semver/classes/semver'; -import { ReindexWarning } from '../../../../../../../common/types'; -import { idForWarning, WarningsFlyoutStep } from './warnings_step'; +import { idForWarning, WarningFlyoutStep } from './warning_step'; +import { IndexWarning } from '../../../../../../../../../common/types'; const kibanaVersion = new SemVer('8.0.0'); -jest.mock('../../../../../app_context', () => { +jest.mock('../../../../../../../app_context', () => { const { docLinksServiceMock } = jest.requireActual('@kbn/core-doc-links-browser-mocks'); return { @@ -31,20 +31,24 @@ jest.mock('../../../../../app_context', () => { }; }); -describe('WarningsFlyoutStep', () => { +describe('WarningFlyoutStep', () => { const defaultProps = { - warnings: [] as ReindexWarning[], - hideWarningsStep: jest.fn(), - continueReindex: jest.fn(), + warnings: [] as IndexWarning[], + back: jest.fn(), + confirm: jest.fn(), + flow: 'reindex' as const, meta: { indexName: 'foo', reindexName: 'reindexed-foo', aliases: [], + isFrozen: false, + isReadonly: false, + isInDataStream: false, }, }; it('renders', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); if (kibanaVersion.major === 7) { @@ -53,28 +57,29 @@ describe('WarningsFlyoutStep', () => { ...defaultProps, warnings: [ { + flow: 'all' as const, warningType: 'indexSetting', meta: { deprecatedSettings: ['index.force_memory_term_dictionary'], }, }, - ] as ReindexWarning[], + ] as IndexWarning[], }; const wrapper = mount( - + ); const button = wrapper.find('EuiButton'); button.simulate('click'); - expect(defaultPropsWithWarnings.continueReindex).not.toHaveBeenCalled(); + expect(defaultPropsWithWarnings.confirm).not.toHaveBeenCalled(); // first warning (indexSetting) wrapper.find(`input#${idForWarning(1)}`).simulate('change'); button.simulate('click'); - expect(defaultPropsWithWarnings.continueReindex).toHaveBeenCalled(); + expect(defaultPropsWithWarnings.confirm).toHaveBeenCalled(); }); } }); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/warning_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/warning_step.tsx new file mode 100644 index 0000000000000..cefb2f03df723 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/warning_step.tsx @@ -0,0 +1,213 @@ +/* + * 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 React, { useState } from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + IndexWarning, + IndexWarningType, + ReindexStatusResponse, +} from '../../../../../../../../../common/types'; +import { useAppContext } from '../../../../../../../app_context'; +import { + DeprecatedSettingWarningCheckbox, + ReplaceIndexWithAliasWarningCheckbox, + MakeIndexReadonlyWarningCheckbox, + WarningCheckboxProps, +} from './warning_step_checkbox'; +import { FrozenCallOut } from '../frozen_callout'; + +interface CheckedIds { + [id: string]: boolean; +} + +const warningToComponentMap: { + [key in IndexWarningType]: React.FunctionComponent; +} = { + indexSetting: DeprecatedSettingWarningCheckbox, + replaceIndexWithAlias: ReplaceIndexWithAliasWarningCheckbox, + makeIndexReadonly: MakeIndexReadonlyWarningCheckbox, +}; + +export const idForWarning = (id: number) => `reindexWarning-${id}`; +interface WarningFlyoutStepProps { + back: () => void; + confirm: () => void; + flow: 'readonly' | 'reindex'; + meta: ReindexStatusResponse['meta']; + warnings: IndexWarning[]; +} + +/** + * Displays warning text about destructive changes required to reindex this index. The user + * must acknowledge each change before being allowed to proceed. + */ +export const WarningFlyoutStep: React.FunctionComponent = ({ + back, + confirm, + flow, + meta, + warnings, +}) => { + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + const { links } = docLinks; + + const [checkedIds, setCheckedIds] = useState( + warnings.reduce((initialCheckedIds, warning, index) => { + initialCheckedIds[idForWarning(index)] = false; + return initialCheckedIds; + }, {} as { [id: string]: boolean }) + ); + + // Do not allow to proceed until all checkboxes are checked. + const blockAdvance = Object.values(checkedIds).filter((v) => v).length < warnings.length; + + const onChange = (e: React.ChangeEvent) => { + const optionId = e.target.id; + + setCheckedIds((prev) => ({ + ...prev, + ...{ + [optionId]: !checkedIds[optionId], + }, + })); + }; + + return ( + <> + + {meta.isFrozen && } + {warnings.length > 0 && ( + <> + {flow === 'reindex' && ( + + } + color="warning" + iconType="warning" + > +

    + +

    +
    + )} + {flow === 'readonly' && ( + + } + color="warning" + iconType="warning" + > +

    + +

    +
    + )} + + + +

    + +

    +
    + + {warnings.map((warning, index) => { + const WarningCheckbox = warningToComponentMap[warning.warningType]; + return ( + + ); + })} + + )} +
    + + + + + + + + + {flow === 'reindex' && ( + + + + )} + {flow === 'readonly' && ( + + + + )} + + + + + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/warning_step_checkbox.tsx similarity index 67% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/warning_step_checkbox.tsx index 27bfdc6256781..8e96c9ee570c5 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/flyout/steps/warning/warning_step_checkbox.tsx @@ -19,11 +19,11 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { DocLinksStart } from '@kbn/core/public'; -import { ReindexWarning, ReindexWarningTypes } from '../../../../../../../common/types'; +import { IndexWarning, IndexWarningType } from '../../../../../../../../../common/types'; -export const hasReindexWarning = ( - warnings: ReindexWarning[], - warningType: ReindexWarningTypes +export const hasIndexWarning = ( + warnings: IndexWarning[], + warningType: IndexWarningType ): boolean => { return Boolean(warnings.find((warning) => warning.warningType === warningType)); }; @@ -53,7 +53,7 @@ const WarningCheckbox: React.FunctionComponent<{ } @@ -79,7 +79,7 @@ export interface WarningCheckboxProps { onChange: (event: React.ChangeEvent) => void; docLinks: DocLinksStart['links']; id: string; - meta?: ReindexWarning['meta']; + meta?: IndexWarning['meta']; } export const DeprecatedSettingWarningCheckbox: React.FunctionComponent = ({ @@ -96,14 +96,14 @@ export const DeprecatedSettingWarningCheckbox: React.FunctionComponent } description={ <> @@ -135,7 +135,7 @@ export const ReplaceIndexWithAliasWarningCheckbox: React.FunctionComponent< warningId={id} label={ {meta?.indexName}, @@ -145,7 +145,7 @@ export const ReplaceIndexWithAliasWarningCheckbox: React.FunctionComponent< } description={ {meta?.indexName}, @@ -156,3 +156,36 @@ export const ReplaceIndexWithAliasWarningCheckbox: React.FunctionComponent< /> ); }; + +export const MakeIndexReadonlyWarningCheckbox: React.FunctionComponent = ({ + isChecked, + onChange, + id, + meta, +}) => { + return ( + {meta?.indexName}, + }} + /> + } + description={ + {meta?.indexName}, + }} + /> + } + /> + ); +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/index.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/index.tsx similarity index 84% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/index.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/index.tsx index bbb1493f15bcc..3ac6d5252a775 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/index.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/index.tsx @@ -5,4 +5,4 @@ * 2.0. */ -export { ReindexTableRow } from './table_row'; +export { IndexTableRow } from './table_row'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/resolution_table_cell.tsx similarity index 57% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/resolution_table_cell.tsx index 7ad71efc53d32..190a1a7023998 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/resolution_table_cell.tsx @@ -19,65 +19,89 @@ import { import { ReindexStatus } from '../../../../../../common/types'; import { getReindexProgressLabel } from '../../../../lib/utils'; import { LoadingState } from '../../../types'; -import { useReindexContext } from './context'; +import { useIndexContext } from './context'; const i18nTexts = { reindexLoadingStatusText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.reindex.reindexLoadingStatusText', + 'xpack.upgradeAssistant.esDeprecations.indices.reindexLoadingStatusText', { defaultMessage: 'Loading status…', } ), reindexInProgressText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.reindex.reindexInProgressText', + 'xpack.upgradeAssistant.esDeprecations.indices.reindexInProgressText', { defaultMessage: 'Reindexing in progress…', } ), reindexCompleteText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.reindex.reindexCompleteText', + 'xpack.upgradeAssistant.esDeprecations.indices.reindexCompleteText', { defaultMessage: 'Reindex complete', } ), reindexFailedText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.reindex.reindexFailedText', + 'xpack.upgradeAssistant.esDeprecations.indices.reindexFailedText', { defaultMessage: 'Reindex failed', } ), reindexFetchFailedText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.reindex.reindexFetchFailedText', + 'xpack.upgradeAssistant.esDeprecations.indices.reindexFetchFailedText', { defaultMessage: 'Reindex status not available', } ), reindexCanceledText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.reindex.reindexCanceledText', + 'xpack.upgradeAssistant.esDeprecations.indices.reindexCanceledText', { defaultMessage: 'Reindex cancelled', } ), reindexPausedText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.reindex.reindexPausedText', + 'xpack.upgradeAssistant.esDeprecations.indices.reindexPausedText', { defaultMessage: 'Reindex paused', } ), - resolutionText: i18n.translate('xpack.upgradeAssistant.esDeprecations.reindex.resolutionLabel', { + reindexText: i18n.translate('xpack.upgradeAssistant.esDeprecations.indices.reindexLabel', { defaultMessage: 'Reindex', }), - resolutionTooltipLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.reindex.resolutionTooltipLabel', + reindexTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.reindexTooltipLabel', + { + defaultMessage: 'Resolve this issue by reindexing into a new, compatible index.', + } + ), + updateText: i18n.translate('xpack.upgradeAssistant.esDeprecations.indices.updateLabel', { + defaultMessage: 'Update', + }), + updateCompleteText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.updateCompleteText', + { + defaultMessage: 'Update complete', + } + ), + updateTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.updateTooltipLabel', { defaultMessage: - 'Resolve this issue by reindexing this index. This issue can be resolved automatically.', + 'Resolve this issue by updating this index. This issue can be resolved automatically either by marking the index as read-only (recommended for large indices) or by reindexing into a new, compatible index.', + } + ), + unfreezeText: i18n.translate('xpack.upgradeAssistant.esDeprecations.indices.unfreezeLabel', { + defaultMessage: 'Unfreeze', + }), + unfreezeTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.indices.unfreezeTooltipLabel', + { + defaultMessage: 'Resolve this issue by unfreezing this index.', } ), }; export const ReindexResolutionCell: React.FunctionComponent = () => { - const { reindexState } = useReindexContext(); + const { reindexState, deprecation, updateIndexState } = useIndexContext(); const hasExistingAliases = reindexState.meta.aliases.length > 0; if (reindexState.loadingState === LoadingState.Loading) { @@ -158,14 +182,51 @@ export const ReindexResolutionCell: React.FunctionComponent = () => { ); } - return ( - + switch (updateIndexState.status) { + case 'complete': + return ( + + + + + + {i18nTexts.updateCompleteText} + + + ); + } + + // reindex status "not started" + return deprecation.correctiveAction?.type === 'unfreeze' ? ( + + + + + + + {i18nTexts.unfreezeText} + + + + ) : reindexState.meta.isReadonly ? ( + + + + + + + {i18nTexts.reindexText} + + + + ) : ( + - {i18nTexts.resolutionText} + {i18nTexts.updateText} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/table_row.tsx similarity index 77% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/table_row.tsx index 1059720e66a59..72ac9b09f1738 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/table_row.tsx @@ -19,8 +19,8 @@ import { import { DeprecationTableColumns } from '../../../types'; import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells'; import { ReindexResolutionCell } from './resolution_table_cell'; -import { ReindexFlyout, ReindexFlyoutProps } from './flyout'; -import { ReindexStatusProvider, useReindexContext } from './context'; +import { IndexFlyout, IndexFlyoutProps } from './flyout'; +import { IndexStatusProvider, useIndexContext } from './context'; const { useGlobalFlyout } = GlobalFlyout; @@ -29,31 +29,30 @@ interface TableRowProps { rowFieldNames: DeprecationTableColumns[]; } -const ReindexTableRowCells: React.FunctionComponent = ({ +const IndexTableRowCells: React.FunctionComponent = ({ rowFieldNames, deprecation, }) => { const [showFlyout, setShowFlyout] = useState(false); - const reindexState = useReindexContext(); + const indexContext = useIndexContext(); const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = useGlobalFlyout(); const closeFlyout = useCallback(async () => { - removeContentFromGlobalFlyout('reindexFlyout'); + removeContentFromGlobalFlyout('indexFlyout'); setShowFlyout(false); uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_CLOSE_FLYOUT_CLICK); }, [removeContentFromGlobalFlyout]); useEffect(() => { if (showFlyout) { - addContentToGlobalFlyout({ - id: 'reindexFlyout', - Component: ReindexFlyout, + addContentToGlobalFlyout({ + id: 'indexFlyout', + Component: IndexFlyout, props: { - deprecation, closeFlyout, - ...reindexState, + ...indexContext, }, flyoutProps: { onClose: closeFlyout, @@ -63,7 +62,7 @@ const ReindexTableRowCells: React.FunctionComponent = ({ }, }); } - }, [addContentToGlobalFlyout, deprecation, showFlyout, reindexState, closeFlyout]); + }, [addContentToGlobalFlyout, deprecation, showFlyout, indexContext, closeFlyout]); useEffect(() => { if (showFlyout) { @@ -93,14 +92,14 @@ const ReindexTableRowCells: React.FunctionComponent = ({ ); }; -export const ReindexTableRow: React.FunctionComponent = (props) => { +export const IndexTableRow: React.FunctionComponent = (props) => { const { services: { api }, } = useAppContext(); return ( - - - + + + ); }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/use_reindex.tsx similarity index 92% rename from x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx rename to x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/use_reindex.tsx index 9c1e5b932efdd..90c87f1607aba 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/use_reindex.tsx @@ -11,7 +11,7 @@ import { ReindexStatusResponse, ReindexStatus, ReindexStep, - ReindexWarning, + IndexWarning, } from '../../../../../../common/types'; import { CancelLoadingState, LoadingState } from '../../../types'; import { ApiService } from '../../../../lib/api'; @@ -25,20 +25,24 @@ export interface ReindexState { status?: ReindexStatus; reindexTaskPercComplete: number | null; errorMessage: string | null; - reindexWarnings?: ReindexWarning[]; + reindexWarnings?: IndexWarning[]; hasRequiredPrivileges?: boolean; meta: { indexName: string; reindexName: string; aliases: string[]; + isFrozen: boolean; + isReadonly: boolean; + isInDataStream: boolean; + isClosedIndex: boolean; }; } const getReindexState = ( reindexState: ReindexState, { reindexOp, warnings, hasRequiredPrivileges, meta: updatedMeta }: ReindexStatusResponse -) => { - const meta = { ...(updatedMeta ?? reindexState.meta) }; +): ReindexState => { + const meta = { ...reindexState.meta, ...updatedMeta }; // Once we have received an array of existing aliases, we won't update the meta value anymore because // when we'll delete the original alias during the reindex process there won't be any aliases pointing // to it anymore and the last reindex step (Update existing aliases) would be suddenly removed. @@ -49,7 +53,6 @@ const getReindexState = ( meta: { ...meta, aliases }, loadingState: LoadingState.Success, }; - if (warnings) { newReindexState.reindexWarnings = warnings; } @@ -107,15 +110,32 @@ const getReindexState = ( return newReindexState; }; -export const useReindexStatus = ({ indexName, api }: { indexName: string; api: ApiService }) => { +export const useReindex = ({ + indexName, + isFrozen, + isInDataStream, + isClosedIndex, + api, +}: { + indexName: string; + isFrozen: boolean; + isInDataStream: boolean; + isClosedIndex: boolean; + api: ApiService; +}) => { const [reindexState, setReindexState] = useState({ loadingState: LoadingState.Loading, errorMessage: null, reindexTaskPercComplete: null, meta: { indexName, - reindexName: '', // will be known after fetching the reindexStatus - aliases: [], // will be known after fetching the reindexStatus + // these properties will be known after fetching the reindexStatus + reindexName: '', + aliases: [], + isFrozen, + isInDataStream, + isClosedIndex, + isReadonly: false, // we don't have this information in the deprecation list }, }); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/use_update_index.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/use_update_index.tsx new file mode 100644 index 0000000000000..7ed23deccc195 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/indices/use_update_index.tsx @@ -0,0 +1,52 @@ +/* + * 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 { useCallback, useState } from 'react'; +import type { UpdateIndexOperation } from '../../../../../../common/update_index'; +import type { CorrectiveAction } from '../../../../../../common/types'; +import type { ApiService } from '../../../../lib/api'; + +export interface UpdateIndexState { + failedBefore: boolean; + status: 'incomplete' | 'inProgress' | 'complete' | 'failed'; + reason?: string; +} + +export interface UseUpdateIndexParams { + indexName: string; + api: ApiService; + correctiveAction?: CorrectiveAction; +} + +export const useUpdateIndex = ({ indexName, api, correctiveAction }: UseUpdateIndexParams) => { + const [failedState, setFailedState] = useState(false); + const [updateIndexState, setUpdateIndexState] = useState({ + failedBefore: false, + status: 'incomplete', + }); + + const updateIndex = useCallback(async () => { + const operations: UpdateIndexOperation[] = + correctiveAction?.type === 'unfreeze' ? ['unfreeze'] : ['blockWrite', 'unfreeze']; + + setUpdateIndexState({ status: 'inProgress', failedBefore: failedState }); + const res = await api.updateIndex(indexName, operations); + const status = res.error ? 'failed' : 'complete'; + const failedBefore = failedState || status === 'failed'; + setFailedState(failedBefore); + setUpdateIndexState({ + status, + failedBefore, + ...(res.error && { reason: res.error.message.toString() }), + }); + }, [api, correctiveAction, failedState, indexName]); + + return { + updateIndexState, + updateIndex, + }; +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/context.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/context.tsx deleted file mode 100644 index 3d4459e7380b7..0000000000000 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/context.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 React, { createContext, useContext } from 'react'; - -import { ApiService } from '../../../../lib/api'; -import { useReindexStatus, ReindexState } from './use_reindex_state'; - -export interface ReindexStateContext { - reindexState: ReindexState; - startReindex: () => Promise; - cancelReindex: () => Promise; -} - -const ReindexContext = createContext(undefined); - -export const useReindexContext = () => { - const context = useContext(ReindexContext); - if (context === undefined) { - throw new Error('useReindexContext must be used within a '); - } - return context; -}; - -interface Props { - api: ApiService; - children: React.ReactNode; - indexName: string; -} - -export const ReindexStatusProvider: React.FunctionComponent = ({ - api, - indexName, - children, -}) => { - const { reindexState, startReindex, cancelReindex } = useReindexStatus({ - indexName, - api, - }); - - return ( - - {children} - - ); -}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx deleted file mode 100644 index b353e18f0b51a..0000000000000 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 React, { useCallback, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFlyoutHeader, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { METRIC_TYPE } from '@kbn/analytics'; - -import { EnrichedDeprecationInfo, ReindexStatus } from '../../../../../../../common/types'; - -import type { ReindexStateContext } from '../context'; -import { ChecklistFlyoutStep } from './checklist_step'; -import { WarningsFlyoutStep } from './warnings_step'; -import { DeprecationBadge } from '../../../../shared'; -import { - UIM_REINDEX_START_CLICK, - UIM_REINDEX_STOP_CLICK, - uiMetricService, -} from '../../../../../lib/ui_metric'; - -export interface ReindexFlyoutProps extends ReindexStateContext { - deprecation: EnrichedDeprecationInfo; - closeFlyout: () => void; -} - -export const ReindexFlyout: React.FunctionComponent = ({ - reindexState, - startReindex, - cancelReindex, - closeFlyout, - deprecation, -}) => { - const { status, reindexWarnings } = reindexState; - const { index } = deprecation; - - const [showWarningsStep, setShowWarningsStep] = useState(false); - - const onStartReindex = useCallback(() => { - uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_START_CLICK); - startReindex(); - }, [startReindex]); - - const onStopReindex = useCallback(() => { - uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_STOP_CLICK); - cancelReindex(); - }, [cancelReindex]); - - const startReindexWithWarnings = () => { - if ( - reindexWarnings && - reindexWarnings.length > 0 && - status !== ReindexStatus.inProgress && - status !== ReindexStatus.completed - ) { - setShowWarningsStep(true); - } else { - onStartReindex(); - } - }; - const flyoutContents = showWarningsStep ? ( - setShowWarningsStep(false)} - continueReindex={() => { - setShowWarningsStep(false); - onStartReindex(); - }} - /> - ) : ( - - ); - - return ( - <> - - - - -

    - -

    -
    -
    - - {flyoutContents} - - ); -}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx deleted file mode 100644 index d3ed1d0c16387..0000000000000 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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 React, { useState } from 'react'; - -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { - ReindexWarning, - ReindexWarningTypes, - ReindexStatusResponse, -} from '../../../../../../../common/types'; -import { useAppContext } from '../../../../../app_context'; -import { - DeprecatedSettingWarningCheckbox, - ReplaceIndexWithAliasWarningCheckbox, - WarningCheckboxProps, -} from './warning_step_checkbox'; - -interface CheckedIds { - [id: string]: boolean; -} - -const warningToComponentMap: { - [key in ReindexWarningTypes]: React.FunctionComponent; -} = { - indexSetting: DeprecatedSettingWarningCheckbox, - replaceIndexWithAlias: ReplaceIndexWithAliasWarningCheckbox, -}; - -export const idForWarning = (id: number) => `reindexWarning-${id}`; -interface WarningsConfirmationFlyoutProps { - hideWarningsStep: () => void; - continueReindex: () => void; - warnings: ReindexWarning[]; - meta: ReindexStatusResponse['meta']; -} - -/** - * Displays warning text about destructive changes required to reindex this index. The user - * must acknowledge each change before being allowed to proceed. - */ -export const WarningsFlyoutStep: React.FunctionComponent = ({ - warnings, - hideWarningsStep, - continueReindex, - meta, -}) => { - const { - services: { - core: { docLinks }, - }, - } = useAppContext(); - const { links } = docLinks; - - const [checkedIds, setCheckedIds] = useState( - warnings.reduce((initialCheckedIds, warning, index) => { - initialCheckedIds[idForWarning(index)] = false; - return initialCheckedIds; - }, {} as { [id: string]: boolean }) - ); - - // Do not allow to proceed until all checkboxes are checked. - const blockAdvance = Object.values(checkedIds).filter((v) => v).length < warnings.length; - - const onChange = (e: React.ChangeEvent) => { - const optionId = e.target.id; - - setCheckedIds((prev) => ({ - ...prev, - ...{ - [optionId]: !checkedIds[optionId], - }, - })); - }; - - return ( - <> - - {warnings.length > 0 && ( - <> - - } - color="warning" - iconType="warning" - > -

    - -

    -
    - - -

    - -

    -
    - - {warnings.map((warning, index) => { - const WarningCheckbox = warningToComponentMap[warning.warningType]; - return ( - - ); - })} - - )} -
    - - - - - - - - - - - - - - - - ); -}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx index f3f9716dc823b..6c48e8b817183 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx @@ -31,7 +31,7 @@ import { MlSnapshotsTableRow, DefaultTableRow, IndexSettingsTableRow, - ReindexTableRow, + IndexTableRow, ClusterSettingsTableRow, HealthIndicatorTableRow, DataStreamTableRow, @@ -126,7 +126,8 @@ const renderTableRowCells = ( return ; case 'reindex': - return ; + case 'unfreeze': + return ; case 'healthIndicator': return ; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx index 472ecccb4f02f..3d92ea07af01a 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx @@ -28,7 +28,7 @@ const i18nTexts = { } ), manualCellTooltipLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.reindex.manualCellTooltipLabel', + 'xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellTooltipLabel', { defaultMessage: 'This issue needs to be resolved manually.', } diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/api.ts b/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/api.ts index 8489e513912e1..80e6a88eec6f4 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/api.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/api.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { HttpSetup } from '@kbn/core/public'; +import type { HttpSetup } from '@kbn/core/public'; -import { +import type { UpdateIndexOperation } from '../../../common/update_index'; +import type { ESUpgradeStatus, CloudBackupStatus, ClusterUpgradeState, @@ -24,9 +25,9 @@ import { CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS, } from '../../../common/constants'; import { - UseRequestConfig, - SendRequestConfig, - SendRequestResponse, + type UseRequestConfig, + type SendRequestConfig, + type SendRequestResponse, sendRequest as _sendRequest, useRequest as _useRequest, } from '../../shared_imports'; @@ -211,41 +212,51 @@ export class ApiService { }); } - public async getDataStreamReindexStatus(dataStreamName: string) { + /** + * Data Stream Migrations + * Reindex and readonly operations + */ + + public async getDataStreamMigrationStatus(dataStreamName: string) { return await this.sendRequest({ - path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}`, + path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}`, method: 'get', }); } public async getDataStreamMetadata(dataStreamName: string) { return await this.sendRequest({ - path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}/metadata`, + path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}/metadata`, method: 'get', }); } public async startDataStreamReindexTask(dataStreamName: string) { return await this.sendRequest({ - path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}`, + path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}/reindex`, method: 'post', }); } public async cancelDataStreamReindexTask(dataStreamName: string) { return await this.sendRequest({ - path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}/cancel`, + path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}/reindex/cancel`, method: 'post', }); } - public async pauseDataStreamReindexTask(dataStreamName: string) { + public async markIndicesAsReadOnly(dataStreamName: string, indices: string[]) { return await this.sendRequest({ - path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}/pause`, + path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}/readonly`, method: 'post', + body: { indices }, }); } + /** + * FINISH: Data Stream Migrations + */ + public async getReindexStatus(indexName: string) { return await this.sendRequest({ path: `${API_BASE_PATH}/reindex/${indexName}`, @@ -267,6 +278,14 @@ export class ApiService { }); } + public async updateIndex(indexName: string, operations: UpdateIndexOperation[]) { + return await this.sendRequest({ + path: `${API_BASE_PATH}/update_index/${indexName}`, + method: 'post', + body: { operations }, + }); + } + public useLoadUpgradeStatus() { return this.useRequest<{ readyForUpgrade: boolean; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/ui_metric.ts b/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/ui_metric.ts index 8b1087a045067..e2e83a5b91861 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/ui_metric.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/ui_metric.ts @@ -19,11 +19,18 @@ export const UIM_REINDEX_OPEN_FLYOUT_CLICK = 'reindex_open_flyout_click'; export const UIM_REINDEX_CLOSE_FLYOUT_CLICK = 'reindex_close_flyout_click'; export const UIM_REINDEX_START_CLICK = 'reindex_start_click'; export const UIM_REINDEX_STOP_CLICK = 'reindex_stop_click'; +export const UIM_REINDEX_READONLY_CLICK = 'reindex_readonly_click'; +export const UIM_REINDEX_UNFREEZE_CLICK = 'reindex_unfreeze_click'; +export const UIM_REINDEX_READONLY_RETRY_CLICK = 'reindex_readonly_retry_click'; +export const UIM_REINDEX_UNFREEZE_RETRY_CLICK = 'reindex_unfreeze_retry_click'; + // Data Streams Reindexing export const UIM_DATA_STREAM_REINDEX_OPEN_FLYOUT_CLICK = 'data_stream_reindex_open_flyout_click'; export const UIM_DATA_STREAM_REINDEX_CLOSE_FLYOUT_CLICK = 'data_stream_reindex_close_flyout_click'; export const UIM_DATA_STREAM_REINDEX_START_CLICK = 'data_stream_reindex_start_click'; export const UIM_DATA_STREAM_REINDEX_STOP_CLICK = 'data_stream_reindex_stop_click'; +export const UIM_DATA_STREAM_START_READONLY_CLICK = 'data_stream_readonly_start_click'; +export const UIM_DATA_STREAM_STOP_READONLY_CLICK = 'data_stream_readonly_stop_click'; export const UIM_BACKUP_DATA_CLOUD_CLICK = 'backup_data_cloud_click'; export const UIM_BACKUP_DATA_ON_PREM_CLICK = 'backup_data_on_prem_click'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/utils.ts b/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/utils.ts index bd6aa8e676d7f..874387428596b 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/utils.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/lib/utils.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { tryCatch, fold } from 'fp-ts/lib/Either'; import { DEPRECATION_WARNING_UPPER_LIMIT } from '../../../common/constants'; -import { ReindexStep, DataStreamReindexStatus } from '../../../common/types'; +import { ReindexStep, DataStreamMigrationStatus } from '../../../common/types'; export const validateRegExpString = (s: string) => pipe( @@ -103,20 +103,20 @@ export const getReindexProgressLabel = ( }; export const getDataStreamReindexProgress = ( - status: DataStreamReindexStatus, - reindexTaskPercComplete: number | null + status: DataStreamMigrationStatus, + taskPercComplete: number | null ): number => { switch (status) { - case DataStreamReindexStatus.notStarted: + case DataStreamMigrationStatus.notStarted: return 0; - case DataStreamReindexStatus.fetchFailed: - case DataStreamReindexStatus.failed: - case DataStreamReindexStatus.cancelled: - case DataStreamReindexStatus.inProgress: { - return reindexTaskPercComplete !== null ? Math.round(reindexTaskPercComplete * 100) : 0; + case DataStreamMigrationStatus.fetchFailed: + case DataStreamMigrationStatus.failed: + case DataStreamMigrationStatus.cancelled: + case DataStreamMigrationStatus.inProgress: { + return taskPercComplete !== null ? Math.round(taskPercComplete * 100) : 0; } - case DataStreamReindexStatus.completed: { + case DataStreamMigrationStatus.completed: { return 100; } } @@ -125,9 +125,9 @@ export const getDataStreamReindexProgress = ( }; export const getDataStreamReindexProgressLabel = ( - status: DataStreamReindexStatus, - reindexTaskPercComplete: number | null + status: DataStreamMigrationStatus, + taskPercComplete: number | null ): string => { - const percentsComplete = getDataStreamReindexProgress(status, reindexTaskPercComplete); + const percentsComplete = getDataStreamReindexProgress(status, taskPercComplete); return `${percentsComplete}%`; }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/config.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/config.ts index ecf168c297c96..36803b41b7543 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/config.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/config.ts @@ -21,6 +21,22 @@ const configSchema = schema.object({ serverless: schema.boolean({ defaultValue: true }), }), + /** + * Exlcude certain data streams or indices from getting certain correctiveActions. + * The key is the data source name or pattern and the value is an array of corrective actions to exclude. + * + * Exclude readOnly data sources from getting read-only corrective actions. + * This is needed to avoid breaking certain built-in/system functionality that might rely on writing to these data sources. + * Example (excludes read-only corrective actions for 7_17_data_stream): + * xpack.upgrade_assistant.dataSourceExclusions: + * 7_17_data_stream: ["readOnly"] + */ + dataSourceExclusions: schema.recordOf( + schema.string(), + schema.arrayOf(schema.oneOf([schema.literal('readOnly'), schema.literal('reindex')])), + { defaultValue: {} } + ), + featureSet: schema.object({ /** * Ml Snapshot should only be enabled for major version upgrades. Currently this @@ -33,12 +49,12 @@ const configSchema = schema.object({ * to change the constant `MachineLearningField.MIN_CHECKED_SUPPORTED_SNAPSHOT_VERSION` * to something higher than 7.0.0 in the Elasticsearch code. */ - mlSnapshots: schema.boolean({ defaultValue: false }), + mlSnapshots: schema.boolean({ defaultValue: true }), /** * Migrating system indices should only be enabled for major version upgrades. * Currently this is manually set to `true` on every `x.last` version. */ - migrateSystemIndices: schema.boolean({ defaultValue: false }), + migrateSystemIndices: schema.boolean({ defaultValue: true }), /** * Deprecations with reindexing corrective actions are only enabled for major version upgrades. * Currently this is manually set to `true` on every `x.last` version. @@ -46,12 +62,12 @@ const configSchema = schema.object({ * The reindex action includes some logic that is specific to the 8.0 upgrade * End users could get into a bad situation if this is enabled before this logic is fixed. */ - reindexCorrectiveActions: schema.boolean({ defaultValue: false }), + reindexCorrectiveActions: schema.boolean({ defaultValue: true }), /** * Migrating deprecated data streams should only be enabled for major version upgrades. * Currently this is manually set to `true` on every `x.last` version. */ - migrateDataStreams: schema.boolean({ defaultValue: false }), + migrateDataStreams: schema.boolean({ defaultValue: true }), }), /** * This config allows to hide the UI without disabling the plugin. diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json index db63d31321078..a1cd4db0dbf0b 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json @@ -67,10 +67,12 @@ { "level": "critical", "message": "Index created before 7.0", - "url": - "https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html", + "url": "https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html", "details": "This index was created using version: 6.8.13", - "resolve_during_rolling_upgrade": false + "resolve_during_rolling_upgrade": false, + "_meta": { + "reindex_required": true + } } ], "frozen_index": [ @@ -98,17 +100,18 @@ "message": "Index created before 7.0", "url": "https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html", "details": "This index was created using version: 6.8.13", - "resolve_during_rolling_upgrade": false + "resolve_during_rolling_upgrade": false, + "_meta": { + "reindex_required": true + } } ], "deprecated_settings": [ { "level": "warning", "message": "Setting [index.routing.allocation.include._tier] is deprecated", - "url": - "https://www.elastic.co/guide/en/elasticsearch/reference/7.16/migrating-7.13.html#deprecate-tier-filter-setting", - "details": - "Remove the [index.routing.allocation.include._tier] setting. Use [index.routing.allocation.include._tier_preference] to control allocation to data tiers.", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/7.16/migrating-7.13.html#deprecate-tier-filter-setting", + "details": "Remove the [index.routing.allocation.include._tier] setting. Use [index.routing.allocation.include._tier_preference] to control allocation to data tiers.", "resolve_during_rolling_upgrade": false, "_meta": { "actions": [ @@ -167,41 +170,137 @@ "resolve_during_rolling_upgrade": false } ], + "transforms_index": [ + { + "level": "critical", + "message": "Old index with a compatibility version < 8.0", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", + "details": "This index has version: 7.17.25", + "resolve_during_rolling_upgrade": false, + "_meta": { + "reindex_required": true, + "transform_ids": ["abc"] + } + } + ], "myindex": [ { "level": "critical", "message": "Old index with a compatibility version < 8.0", "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", "details": "This index has version: 7.17.25", - "resolve_during_rolling_upgrade": false + "resolve_during_rolling_upgrade": false, + "_meta": { + "reindex_required": true + } + } + ], + ".ent-search-1": [ + { + "level": "critical", + "message": "Old index with a compatibility version < 8.0", + "url": "https: //www.elastic.co/guide/en/elasticsearch/reference/current/migrating-8.0.html#breaking-changes-8.0", + "details": "This index has version: 7.17.28-8.0.0", + "resolve_during_rolling_upgrade": false, + "_meta": { + "reindex_required": true + } + } + ], + ".ent-search-2": [ + { + "level": "critical", + "message": "Old index with a compatibility version < 8.0", + "url": "https: //www.elastic.co/guide/en/elasticsearch/reference/current/migrating-8.0.html#breaking-changes-8.0", + "details": "This index has version: 7.17.28-8.0.0", + "resolve_during_rolling_upgrade": false, + "_meta": { + "reindex_required": true + } + } + ], + ".ent-search-3": [ + { + "level": "critical", + "message": "Old index with a compatibility version < 8.0", + "url": "https: //www.elastic.co/guide/en/elasticsearch/reference/current/migrating-8.0.html#breaking-changes-8.0", + "details": "This index has version: 7.17.28-8.0.0", + "resolve_during_rolling_upgrade": false, + "_meta": { + "reindex_required": true + } } ] }, "data_streams": { - "my-v7-data-stream" : [{ - "level" : "critical", - "message" : "Old data stream with a compatibility version < 8.0", - "url" : "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", - "details" : "This data stream has backing indices that were created before Elasticsearch 8.0.0", - "resolve_during_rolling_upgrade" : false, - "_meta": { - "backing_indices": { - "count": 52, - "need_upgrading": { - "count": 37, - "searchable_snapshot": { - "count": 23, - "fully_mounted": { - "count": 7 - }, - "partially_mounted": { - "count": 16 - } - } - } + "my-v7-data-stream": [ + { + "level": "critical", + "message": "Old data stream with a compatibility version < 8.0", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", + "details": "This data stream has backing indices that were created before Elasticsearch 8.0.0", + "resolve_during_rolling_upgrade": false, + "_meta": { + "indices_requiring_upgrade": [ + ".ds-some-backing-index-5-2024.11.07-000001" + ], + "indices_requiring_upgrade_count": 1, + "total_backing_indices": 2, + "reindex_required": true } } - }] + ], + "logs-enterprise_search.default": [ + { + "level": "critical", + "message": "Old data stream with a compatibility version < 8.0", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", + "details": "This data stream has backing indices that were created before Elasticsearch 8.0.0", + "resolve_during_rolling_upgrade": false, + "_meta": { + "indices_requiring_upgrade": [ + ".ds-some-backing-index-5-2024.11.07-000001" + ], + "indices_requiring_upgrade_count": 1, + "total_backing_indices": 2, + "reindex_required": true + } + } + ], + "logs-app_search.default": [ + { + "level": "critical", + "message": "Old data stream with a compatibility version < 8.0", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", + "details": "This data stream has backing indices that were created before Elasticsearch 8.0.0", + "resolve_during_rolling_upgrade": false, + "_meta": { + "indices_requiring_upgrade": [ + ".ds-some-backing-index-5-2024.11.07-000001" + ], + "indices_requiring_upgrade_count": 1, + "total_backing_indices": 2, + "reindex_required": true + } + } + ], + "logs-workplace_search.default": [ + { + "level": "critical", + "message": "Old data stream with a compatibility version < 8.0", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", + "details": "This data stream has backing indices that were created before Elasticsearch 8.0.0", + "resolve_during_rolling_upgrade": false, + "_meta": { + "indices_requiring_upgrade": [ + ".ds-some-backing-index-5-2024.11.07-000001" + ], + "indices_requiring_upgrade_count": 1, + "total_backing_indices": 2, + "reindex_required": true + } + } + ] }, "ilm_policies": { "myfreezepolicy": [ @@ -224,4 +323,4 @@ } ] } -} +} \ No newline at end of file diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/__fixtures__/health_indicators.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/__fixtures__/health_indicators.ts index b56e60dc56f1f..e62f8cbb43852 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/__fixtures__/health_indicators.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/__fixtures__/health_indicators.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { estypes } from '@elastic/elasticsearch'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; export const diskIndicatorGreen: estypes.HealthReportDiskIndicator = { status: 'green', diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_source_exclusions.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_source_exclusions.test.ts new file mode 100644 index 0000000000000..00d8921cf157f --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_source_exclusions.test.ts @@ -0,0 +1,38 @@ +/* + * 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 { matchExclusionPattern } from './data_source_exclusions'; +import { DataSourceExclusions } from '../../common/types'; + +describe('matchExclusionPattern', () => { + it('should return the actions that should be excluded', () => { + const exclusions: DataSourceExclusions = { + data_stream_1: ['readOnly'], + }; + + const result = matchExclusionPattern('data_stream_1', exclusions); + expect(result).toEqual(['readOnly']); + }); + + it('should return an empty array if no exclusions match', () => { + const exclusions: DataSourceExclusions = { + data_stream_1: ['readOnly'], + }; + + const result = matchExclusionPattern('data_stream_2', exclusions); + expect(result).toEqual([]); + }); + + it(`should match patterns ending with '*'`, () => { + const exclusions: DataSourceExclusions = { + 'data_stream_*': ['readOnly'], + }; + + const result = matchExclusionPattern('data_stream_1', exclusions); + expect(result).toEqual(['readOnly']); + }); +}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_source_exclusions.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_source_exclusions.ts new file mode 100644 index 0000000000000..da2247f2a46f6 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_source_exclusions.ts @@ -0,0 +1,60 @@ +/* + * 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 { DataSourceExclusions } from '../../common/types'; + +/** + * These are the default exclusions for data sources (data streams and indices). + * + * They are used to exclude migrations from getting certain corrective actions. + * This is needed to avoid breaking certain built-in/system functionality that might rely on writing to these data source. + * + * These indices can be overridden by the user in the Kibana configuration: + * + * For Example this will renenable all corrective actions for the siem-signals data source: + * xpack.upgrade_assistant.dataSourceExclusions: + * '.siem-signals*': [] + */ +export const defaultExclusions: DataSourceExclusions = { + '.siem-signals*': ['readOnly'], + '.alerts*': ['readOnly'], + '.internal.alerts*': ['readOnly'], + '.preview.alerts*': ['readOnly'], + '.internal.preview.alerts*': ['readOnly'], + '.lists-*': ['readOnly'], + '.items-*': ['readOnly'], + '.logs-endpoint.actions-*': ['readOnly'], + '.logs-endpoint.action.responses-*': ['readOnly'], + '.metrics-endpoint.metadata_united_default': ['readOnly'], + '.logs-osquery_manager.actions-*': ['readOnly'], + '.logs-osquery_manager.action.responses-*': ['readOnly'], + '.logs-endpoint.diagnostic.collection-*': ['readOnly'], + 'kibana_sample_data_*': ['readOnly'], + '.elastic-connectors*': ['readOnly'], +}; + +/** + * Matches the data source name against the exclusion pattern and returns the actions that should be excluded. + * If the exclusion ends with a `*` it will match any data source that starts with the excluded pattern. + * Otherwise it will match the data source name exactly. + */ +export const matchExclusionPattern = (dataStreamName: string, exclusions: DataSourceExclusions) => { + const result = Object.entries(exclusions).find(([excludedPattern]) => { + const isPattern = /.+\*$/.test(excludedPattern); + if (isPattern) { + const matcher = excludedPattern.slice(0, -1); + return dataStreamName.startsWith(matcher); + } + return dataStreamName === excludedPattern; + }); + + if (!result) { + return []; + } + + return result[1]; +}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/data_stream_reindex_service.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/data_stream_migration_service.ts similarity index 62% rename from x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/data_stream_reindex_service.ts rename to x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/data_stream_migration_service.ts index 5411f1e326e2a..5089d542b30eb 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/data_stream_reindex_service.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/data_stream_migration_service.ts @@ -13,31 +13,31 @@ import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import { TransportResult } from '@elastic/elasticsearch'; import _ from 'lodash'; import { - DataStreamReindexStatus, - DataStreamReindexOperation, + DataStreamMigrationStatus, + DataStreamMigrationOperation, DataStreamMetadata, - DataStreamReindexWarning, + DataStreamMigrationWarning, DataStreamReindexTaskStatusResponse, DataStreamReindexStatusCancelled, } from '../../../common/types'; -import { error } from './error'; +import { DataStreamMigrationError, error } from './error'; -interface DataStreamReindexService { +interface DataStreamMigrationService { /** - * Checks whether or not the user has proper privileges required to reindex this index. + * Checks whether or not the user has proper privileges required to migrate this index. * @param dataStreamName */ hasRequiredPrivileges: (dataStreamName: string) => Promise; /** - * Checks an index's settings and mappings to flag potential issues during reindex. + * Checks an index's settings and mappings to flag potential issues during migration. * Resolves to null if index does not exist. * @param dataStreamName */ - detectReindexWarnings: ( + detectMigrationWarnings: ( dataStreamName: string - ) => Promise; + ) => Promise; /** * Creates a new reindex operation for a given index. @@ -49,7 +49,7 @@ interface DataStreamReindexService { * Polls Elasticsearch's Data stream status API to retrieve the status of the reindex operation. * @param dataStreamName */ - fetchReindexStatus: (dataStreamName: string) => Promise; + fetchMigrationStatus: (dataStreamName: string) => Promise; /** * Cancels an in-progress reindex operation for a given index. @@ -62,18 +62,27 @@ interface DataStreamReindexService { * @param dataStreamName */ getDataStreamMetadata: (dataStreamName: string) => Promise; + + /** + * Marks the given indices as read-only. + * First it will roll over the write index if it exists in the deprecated indices. + * Then it will unfreeze the indices and set them to read-only. + * @param dataStreamName + * @param indices + */ + readonlyIndices: (dataStreamName: string, indices: string[]) => Promise; } -export interface DataStreamReindexServiceFactoryParams { +export interface DataStreamMigrationServiceFactoryParams { esClient: ElasticsearchClient; log: Logger; licensing: LicensingPluginSetup; } -export const dataStreamReindexServiceFactory = ({ +export const dataStreamMigrationServiceFactory = ({ esClient, licensing, -}: DataStreamReindexServiceFactoryParams): DataStreamReindexService => { +}: DataStreamMigrationServiceFactoryParams): DataStreamMigrationService => { return { hasRequiredPrivileges: async (dataStreamName: string): Promise => { /** @@ -105,10 +114,15 @@ export const dataStreamReindexServiceFactory = ({ return resp.has_all_requested; }, - async detectReindexWarnings(): Promise { + async detectMigrationWarnings(): Promise { return [ + { + warningType: 'affectExistingSetups', + resolutionType: 'readonly', + }, { warningType: 'incompatibleDataStream', + resolutionType: 'reindex', }, ]; }, @@ -149,7 +163,7 @@ export const dataStreamReindexServiceFactory = ({ ); } }, - async fetchReindexStatus(dataStreamName: string): Promise { + async fetchMigrationStatus(dataStreamName: string): Promise { // Check reindexing task progress try { const taskResponse = await esClient.transport.request({ @@ -169,24 +183,58 @@ export const dataStreamReindexServiceFactory = ({ ); } + // Propagate errors from the reindex task even if reindexing is not yet complete. + if (taskResponse.errors.length) { + // Include the entire task result in the error message. This should be guaranteed + // to be JSON-serializable since it just came back from Elasticsearch. + throw error.reindexTaskFailed( + `Reindexing failed with ${taskResponse.errors.length} errors:\n${JSON.stringify( + taskResponse, + null, + 2 + )}` + ); + } + if (taskResponse.complete) { - // Check that no failures occurred - if (taskResponse.errors.length) { - // Include the entire task result in the error message. This should be guaranteed - // to be JSON-serializable since it just came back from Elasticsearch. - throw error.reindexTaskFailed( - `Reindexing failed with ${taskResponse.errors.length} errors:\n${JSON.stringify( - taskResponse, - null, - 2 - )}` + /** + * If the task is complete, check if there are any remaining indices that require upgrade + * If that is the case, we need to update the status to not started + * This way the user can trigger a new migration. + * Note: This is the best place to do this call because we it'll only be called + * 1 timeonce the task is complete. + * Cases we reach this code execution: + * 1. Task is complete and the user has the UA open. It'll disappear once the user refreshes. + * 2. Task is complete but we have remaining indices that require upgrade. + */ + + const { data_streams: dataStreamsDeprecations } = await esClient.migration.deprecations({ + filter_path: `data_streams`, + }); + + const deprecationsDetails = dataStreamsDeprecations[dataStreamName]; + if (deprecationsDetails && deprecationsDetails.length) { + const deprecationDetails = deprecationsDetails.find( + (deprecation) => deprecation._meta!.reindex_required ); + if (deprecationDetails) { + const stillNeedsUpgrade = + deprecationDetails._meta!.reindex_required === true && + deprecationDetails._meta!.indices_requiring_upgrade_count > 0; + if (stillNeedsUpgrade) { + return { + status: DataStreamMigrationStatus.notStarted, + }; + } + } } + // Find the first deprecation that has reindex_required set to true // Update the status return { - reindexTaskPercComplete: 1, - status: DataStreamReindexStatus.completed, + taskPercComplete: 1, + status: DataStreamMigrationStatus.completed, + resolutionType: 'reindex', progressDetails: { startTimeMs: taskResponse.start_time_millis, successCount: taskResponse.successes, @@ -200,8 +248,9 @@ export const dataStreamReindexServiceFactory = ({ const perc = taskResponse.successes / taskResponse.total_indices_in_data_stream; return { - status: DataStreamReindexStatus.inProgress, - reindexTaskPercComplete: perc, + status: DataStreamMigrationStatus.inProgress, + taskPercComplete: perc, + resolutionType: 'reindex', progressDetails: { startTimeMs: taskResponse.start_time_millis, successCount: taskResponse.successes, @@ -219,12 +268,13 @@ export const dataStreamReindexServiceFactory = ({ // cancelled, never started, or successful task but finished from than 24 hours ago // Since this API should be called as a follow up from _migrate API, we can assume that the task is not started return { - status: DataStreamReindexStatus.notStarted, + status: DataStreamMigrationStatus.notStarted, }; } return { - status: DataStreamReindexStatus.failed, + status: DataStreamMigrationStatus.failed, + resolutionType: 'reindex', errorMessage: err.toString(), }; } @@ -240,7 +290,8 @@ export const dataStreamReindexServiceFactory = ({ } return { - status: DataStreamReindexStatus.cancelled, + status: DataStreamMigrationStatus.cancelled, + resolutionType: 'reindex', }; }, async getDataStreamMetadata(dataStreamName: string): Promise { @@ -285,9 +336,9 @@ export const dataStreamReindexServiceFactory = ({ throw error.cannotGrabMetadata(`Index ${index} does not exist in this cluster.`); } - indicesRequiringUpgradeDocsSize += (indexStats[1] as any).total.store + indicesRequiringUpgradeDocsSize += (indexStats[1] as any).primaries.store .total_data_set_size_in_bytes; - indicesRequiringUpgradeDocsCount += (indexStats[1] as any).total.docs.count; + indicesRequiringUpgradeDocsCount += (indexStats[1] as any).primaries.docs.count; const body = await esClient.indices.getSettings({ index, @@ -319,5 +370,49 @@ export const dataStreamReindexServiceFactory = ({ ); } }, + + async readonlyIndices(dataStreamName: string, indices: string[]) { + try { + const { data_streams: dataStreamsDetails } = await esClient.indices.getDataStream({ + name: dataStreamName, + }); + // Since we are not using a pattern it should only return one item + const dataStreamBackIndices = dataStreamsDetails[0].indices; + + // The last item in this array contains information about the stream’s current write index. + const writeIndex = dataStreamBackIndices[dataStreamBackIndices.length - 1].index_name; + const hasWriteIndex = indices.some((index) => index === writeIndex); + + if (hasWriteIndex) { + const rollOverResponse = await esClient.indices.rollover({ + alias: dataStreamName, + }); + if (!rollOverResponse.acknowledged) { + throw error.readonlyTaskFailed(`Could not rollover data stream ${dataStreamName}.`); + } + } + } catch (err) { + throw error.readonlyTaskFailed(`Could not migrate data stream ${dataStreamName}.`); + } + + for (const index of indices) { + try { + const addBlock = await esClient.indices.addBlock({ index, block: 'write' }); + + if (!addBlock.acknowledged) { + throw error.readonlyTaskFailed(`Could not set index ${index} to readonly.`); + } + } catch (err) { + if (err instanceof DataStreamMigrationError) { + throw err; + } + // ES errors are serializable, so we can just stringify the error and throw it. + const stringifiedErr = JSON.stringify(err, null, 2); + throw error.readonlyTaskFailed( + `Could not migrate index "${index}". Got: ${stringifiedErr}` + ); + } + } + }, }; }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/error.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/error.ts index 7f5ce882aa8bf..8410e32b3809c 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/error.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/error.ts @@ -12,16 +12,17 @@ import { ReindexAlreadyInProgress, ReindexCannotBeCancelled, MetadataCannotBeGrabbed, + ReadonlyTaskFailed, } from './error_symbols'; -export class ReindexError extends Error { +export class DataStreamMigrationError extends Error { constructor(message: string, public readonly symbol: symbol) { super(message); } } export const createErrorFactory = (symbol: symbol) => (message: string) => { - return new ReindexError(message, symbol); + return new DataStreamMigrationError(message, symbol); }; export const error = { @@ -31,4 +32,5 @@ export const error = { reindexTaskFailed: createErrorFactory(ReindexTaskFailed), reindexAlreadyInProgress: createErrorFactory(ReindexAlreadyInProgress), reindexCannotBeCancelled: createErrorFactory(ReindexCannotBeCancelled), + readonlyTaskFailed: createErrorFactory(ReadonlyTaskFailed), }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/error_symbols.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/error_symbols.ts index f25cbf5cbc243..2352ed8a374fa 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/error_symbols.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/error_symbols.ts @@ -11,3 +11,4 @@ export const ReindexTaskFailed = Symbol('ReindexTaskFailed'); export const ReindexAlreadyInProgress = Symbol('ReindexAlreadyInProgress'); export const ReindexCannotBeCancelled = Symbol('ReindexCannotBeCancelled'); export const MetadataCannotBeGrabbed = Symbol('MetadataCannotBeGrabbed'); +export const ReadonlyTaskFailed = Symbol('ReadonlyTaskFailed'); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/index.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/index.ts index 78db1720b3424..7ed4c04e9b6b5 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/index.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/data_streams/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { dataStreamReindexServiceFactory } from './data_stream_reindex_service'; +export { dataStreamMigrationServiceFactory } from './data_stream_migration_service'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations.test.ts new file mode 100644 index 0000000000000..94f46c124831a --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { DeprecationDetailsMessage, DeprecationsDetails } from '@kbn/core-deprecations-common'; +import { GetDeprecationsContext } from '@kbn/core-deprecations-server'; + +import { getEnterpriseSearchPre8IndexDeprecations } from './enterprise_search_deprecations'; +import indexDeprecatorFxns = require('./pre_eight_index_deprecator'); + +const ctx = { + esClient: { + asInternalUser: {}, + }, +} as GetDeprecationsContext; + +function getMessageFromDeprecation(details: DeprecationsDetails): string { + const message = details.message as DeprecationDetailsMessage; + return message.content; +} + +describe('getEnterpriseSearchPre8IndexDeprecations', () => { + it('can register index and data stream deprecations that need to be set to read only', async () => { + const getIndicesMock = jest.fn(() => + Promise.resolve([ + { + name: '.ent-search-index_without_datastream', + hasDatastream: false, + datastreams: [], + }, + { + name: '.ent-search-with_data_stream', + hasDatastream: true, + datastreams: ['datastream-testing'], + }, + ]) + ); + + jest + .spyOn(indexDeprecatorFxns, 'getPreEightEnterpriseSearchIndices') + .mockImplementation(getIndicesMock); + + const deprecations = await getEnterpriseSearchPre8IndexDeprecations(ctx, 'docsurl'); + expect(deprecations).toHaveLength(1); + expect(deprecations[0].correctiveActions.api?.path).toStrictEqual( + '/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only' + ); + expect(deprecations[0].title).toMatch('Pre 8.x Enterprise Search indices compatibility'); + expect(getMessageFromDeprecation(deprecations[0])).toContain( + 'The following indices are found to be incompatible for upgrade' + ); + expect(getMessageFromDeprecation(deprecations[0])).toContain( + '.ent-search-index_without_datastream' + ); + expect(getMessageFromDeprecation(deprecations[0])).toContain( + 'The following data streams are found to be incompatible for upgrade' + ); + expect(getMessageFromDeprecation(deprecations[0])).toContain('.ent-search-with_data_stream'); + }); + + it('can register an index without data stream deprecations that need to be set to read only', async () => { + const getIndicesMock = jest.fn(() => + Promise.resolve([ + { + name: '.ent-search-index_without_datastream', + hasDatastream: false, + datastreams: [''], + }, + ]) + ); + + jest + .spyOn(indexDeprecatorFxns, 'getPreEightEnterpriseSearchIndices') + .mockImplementation(getIndicesMock); + + const deprecations = await getEnterpriseSearchPre8IndexDeprecations(ctx, 'docsurl'); + expect(deprecations).toHaveLength(1); + expect(deprecations[0].correctiveActions.api?.path).toStrictEqual( + '/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only' + ); + expect(deprecations[0].title).toMatch('Pre 8.x Enterprise Search indices compatibility'); + expect(getMessageFromDeprecation(deprecations[0])).toContain( + 'The following indices are found to be incompatible for upgrade' + ); + expect(getMessageFromDeprecation(deprecations[0])).toContain( + '.ent-search-index_without_datastream' + ); + expect(getMessageFromDeprecation(deprecations[0])).not.toContain( + 'The following data streams are found to be incompatible for upgrade' + ); + expect(getMessageFromDeprecation(deprecations[0])).not.toContain( + '.ent-search-with_data_stream' + ); + }); +}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations.ts new file mode 100644 index 0000000000000..311fe1f797007 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations.ts @@ -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. + */ + +import { DeprecationsDetails } from '@kbn/core-deprecations-common'; +import { GetDeprecationsContext, RegisterDeprecationsConfig } from '@kbn/core-deprecations-server'; + +import { i18n } from '@kbn/i18n'; +import { getPreEightEnterpriseSearchIndices } from './pre_eight_index_deprecator'; + +export const getEntepriseSearchRegisteredDeprecations = ( + docsUrl: string +): RegisterDeprecationsConfig => { + return { + getDeprecations: async (ctx: GetDeprecationsContext) => { + const [entSearchIndexIncompatibility] = await Promise.all([ + getEnterpriseSearchPre8IndexDeprecations(ctx, docsUrl), + ]); + return [...entSearchIndexIncompatibility]; + }, + }; +}; + +/** + * If there are any Enterprise Search indices that were created with Elasticsearch 7.x, they must be removed + * or set to read-only + */ +export async function getEnterpriseSearchPre8IndexDeprecations( + ctx: GetDeprecationsContext, + docsUrl: string +): Promise { + const deprecations: DeprecationsDetails[] = []; + + const entSearchIndices = await getPreEightEnterpriseSearchIndices(ctx.esClient.asInternalUser); + if (!entSearchIndices || entSearchIndices.length === 0) { + return deprecations; + } + + let indicesList = ''; + let datastreamsList = ''; + for (const index of entSearchIndices) { + if (index.hasDatastream) { + indicesList += `${index.name}\n`; + for (const datastream of index.datastreams) { + if (datastream === '') continue; + datastreamsList += `${datastream}\n`; + } + } else { + indicesList += `${index.name}\n`; + } + } + + let message = `There are ${entSearchIndices.length} incompatible Enterprise Search indices.\n\n`; + + if (indicesList.length > 0) { + message += + 'The following indices are found to be incompatible for upgrade:\n\n' + + '```\n' + + `${indicesList}` + + '\n```\n' + + 'These indices must be either set to read-only or deleted before upgrading.\n'; + } + + if (datastreamsList.length > 0) { + message += + '\nThe following data streams are found to be incompatible for upgrade:\n\n' + + '```\n' + + `${datastreamsList}` + + '\n```\n' + + 'Using the "quick resolve" button below will roll over any datastreams and set all incompatible indices to read-only.\n\n' + + 'Alternatively, manually deleting these indices and data streams will also unblock your upgrade.'; + } else { + message += + 'Setting these indices to read-only can be attempted with the "quick resolve" button below.\n\n' + + 'Alternatively, manually deleting these indices will also unblock your upgrade.'; + } + + deprecations.push({ + level: 'critical', + deprecationType: 'feature', + title: i18n.translate( + 'xpack.upgradeAssistant.deprecations.incompatibleEnterpriseSearchIndexes.title', + { + defaultMessage: 'Pre 8.x Enterprise Search indices compatibility', + } + ), + message: { + type: 'markdown', + content: i18n.translate( + 'xpack.upgradeAssistant.deprecations.incompatibleEnterpriseSearchIndexes.message', + { + defaultMessage: message, + } + ), + }, + documentationUrl: docsUrl, + correctiveActions: { + manualSteps: [ + i18n.translate( + 'xpack.upgradeAssistant.deprecations.incompatibleEnterpriseSearchIndexes.deleteIndices', + { + defaultMessage: 'Set all incompatible indices and data streams to read only, or', + } + ), + i18n.translate( + 'xpack.upgradeAssistant.deprecations.incompatibleEnterpriseSearchIndexes.deleteIndices', + { + defaultMessage: 'Delete all incompatible indices and data streams', + } + ), + ], + api: { + method: 'POST', + path: '/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only', + body: { + deprecationDetails: { domainId: 'enterpriseSearch' }, + }, + }, + }, + }); + + return deprecations; +} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations_routes.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations_routes.test.ts new file mode 100644 index 0000000000000..7b885729daddd --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations_routes.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { kibanaResponseFactory } from '@kbn/core/server'; + +import { handleEsError } from '../../shared_imports'; +import { + createMockRouter, + MockRouter, + routeHandlerContextMock, +} from '../../routes/__mocks__/routes.mock'; +import { createRequestMock } from '../../routes/__mocks__/request.mock'; + +jest.mock('../es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +import indexDeprecatorFxns = require('./pre_eight_index_deprecator'); + +import { registerEnterpriseSearchDeprecationRoutes } from './enterprise_search_deprecations_routes'; + +describe('deprecation routes', () => { + let routeDependencies: any; + + describe('POST /internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only', () => { + let mockRouter: MockRouter; + + function registerMockRouter({ mlSnapshots } = { mlSnapshots: true }) { + mockRouter = createMockRouter(); + routeDependencies = { + config: { + featureSet: { mlSnapshots, migrateSystemIndices: true, reindexCorrectiveActions: true }, + }, + router: mockRouter, + lib: { handleEsError }, + }; + registerEnterpriseSearchDeprecationRoutes(routeDependencies); + } + + beforeEach(() => { + registerMockRouter(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('sets read-only and 200s correctly in happy path', async () => { + const setIndicesReadOnlyMock = jest.spyOn( + indexDeprecatorFxns, + 'setPreEightEnterpriseSearchIndicesReadOnly' + ); + + setIndicesReadOnlyMock.mockResolvedValue(''); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: + '/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only', + })( + routeHandlerContextMock, + createRequestMock({ + body: { deprecationDetails: { domainId: 'enterpriseSearch' } }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + acknowedged: true, + }); + + expect(setIndicesReadOnlyMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations_routes.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations_routes.ts new file mode 100644 index 0000000000000..8d81c4eef43d7 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/enterprise_search_deprecations_routes.ts @@ -0,0 +1,33 @@ +/* + * 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 { setPreEightEnterpriseSearchIndicesReadOnly } from './pre_eight_index_deprecator'; +import { versionCheckHandlerWrapper } from '../es_version_precheck'; +import { RouteDependencies } from '../../types'; + +export function registerEnterpriseSearchDeprecationRoutes({ router }: RouteDependencies) { + router.post( + { + path: '/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only', + validate: {}, + }, + versionCheckHandlerWrapper(async ({ core }, request, response) => { + const { client } = (await core).elasticsearch; + const setResponse = await setPreEightEnterpriseSearchIndicesReadOnly(client.asCurrentUser); + if (setResponse.length > 0) { + return response.badRequest({ + body: { message: setResponse }, + headers: { 'content-type': 'application/json' }, + }); + } + return response.ok({ + body: { acknowedged: true }, + headers: { 'content-type': 'application/json' }, + }); + }) + ); +} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/index.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/index.ts new file mode 100644 index 0000000000000..2a066c134298a --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { + ENT_SEARCH_DATASTREAM_PATTERN, + ENT_SEARCH_DATASTREAM_PREFIXES, + ENT_SEARCH_INDEX_PREFIX, +} from './pre_eight_index_deprecator'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/pre_eight_index_deprecator.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/pre_eight_index_deprecator.test.ts new file mode 100644 index 0000000000000..78355e18ea1e3 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/pre_eight_index_deprecator.test.ts @@ -0,0 +1,276 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; + +import { + getPreEightEnterpriseSearchIndices, + setPreEightEnterpriseSearchIndicesReadOnly, +} from './pre_eight_index_deprecator'; +import type { + IndicesDataStream, + IndicesGetDataStreamResponse, + IndicesGetResponse, + IndicesIndexState, +} from '@elastic/elasticsearch/lib/api/types'; + +const testIndices = { + '.ent-search-already_read_only': { + settings: { + index: { + version: { + created: '7.0.0', + }, + blocks: { + write: 'true', + }, + verified_read_only: 'true', + }, + }, + data_stream: 'datastream-123', + }, + '.ent-search-post_7_index': { + settings: { + index: { + version: { + created: '8.0.0', + }, + }, + }, + }, + '.ent-search-index_without_datastream': { + settings: { + index: { + version: { + created: '7.0.0', + }, + }, + }, + }, + '.ent-search-with_data_stream': { + settings: { + index: { + version: { + created: '7.0.0', + }, + }, + }, + data_stream: 'datastream-testing', + }, + '.ent-search-with_another_data_stream': { + settings: { + index: { + version: { + created: '7.0.0', + }, + }, + }, + data_stream: 'datastream-testing-another', + }, + '.ent-search-with_same_data_stream': { + settings: { + index: { + version: { + created: '7.0.0', + }, + }, + }, + data_stream: 'datastream-testing', + }, +}; + +const testBackingIndex = { + '.ds-some-other-backing-index': { + settings: { + index: { + version: { + created: '7.0.0', + }, + }, + }, + data_stream: 'logs-app_search.testdatastream', + }, +}; + +const additionalDatastreams: Record = { + 'logs-app_search.testdatastream': { + name: 'logs-app_search.testdatastream', + indices: [ + { index_name: '.ds-some-other-backing-index', index_uuid: '1' }, + { index_name: '.ent-search-with_same_data_stream', index_uuid: '2' }, + ], + } as IndicesDataStream, +}; + +const testIndicesWithoutDatastream: Record = { + '.ent-search-already_read_only': { + settings: { + index: { + version: { + created: '7.0.0', + }, + blocks: { + write: 'true', + }, + verified_read_only: 'true', + }, + }, + }, + '.ent-search-post_7_index': { + settings: { + index: { + version: { + created: '8.0.0', + }, + }, + }, + }, + '.ent-search-index_without_datastream': { + settings: { + index: { + version: { + created: '7.0.0', + }, + }, + }, + }, +}; + +function getMockIndicesFxn(values: Record) { + return () => { + const ret: IndicesGetResponse = {}; + for (const [index, indexData] of Object.entries(values)) { + ret[index] = indexData; + } + return Promise.resolve(ret); + }; +} + +function getMockDatastreamsFxn(values: Record) { + return () => { + const ret: IndicesGetDataStreamResponse = { data_streams: [] }; + for (const [, datastreamData] of Object.entries(values)) { + ret.data_streams.push(datastreamData); + } + return Promise.resolve(ret); + }; +} + +describe('getPreEightEnterpriseSearchIndices', () => { + let esClientMock: ElasticsearchClient; + let getIndicesMock: jest.Mock; + let getDatastreamsMock: jest.Mock; + beforeEach(() => { + getIndicesMock = jest.fn(); + getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testIndices)); + getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testBackingIndex)); + + getDatastreamsMock = jest.fn(getMockDatastreamsFxn(additionalDatastreams)); + esClientMock = { + indices: { + get: getIndicesMock, + getDataStream: getDatastreamsMock, + }, + } as unknown as ElasticsearchClient; + }); + + it('returns the correct indices', async () => { + const indices = await getPreEightEnterpriseSearchIndices(esClientMock); + expect(indices).toEqual([ + { + name: '.ent-search-index_without_datastream', + hasDatastream: false, + datastreams: [''], + }, + { + name: '.ent-search-with_data_stream', + hasDatastream: true, + datastreams: ['datastream-testing'], + }, + { + name: '.ent-search-with_another_data_stream', + hasDatastream: true, + datastreams: ['datastream-testing-another'], + }, + { + name: '.ent-search-with_same_data_stream', + hasDatastream: true, + datastreams: ['datastream-testing'], + }, + { + name: '.ds-some-other-backing-index', + hasDatastream: true, + datastreams: ['logs-app_search.testdatastream'], + }, + ]); + expect(getIndicesMock).toHaveBeenCalledTimes(2); + expect(getIndicesMock).toHaveBeenNthCalledWith(1, { + expand_wildcards: ['all', 'hidden'], + ignore_unavailable: true, + index: '.ent-search-*', + }); + expect(getIndicesMock).toHaveBeenNthCalledWith(2, { + ignore_unavailable: true, + index: ['.ds-some-other-backing-index'], + }); + + expect(getDatastreamsMock).toHaveBeenCalledTimes(1); + expect(getDatastreamsMock).toHaveBeenCalledWith({ + expand_wildcards: ['all', 'hidden'], + name: 'logs-enterprise_search.*,logs-app_search.*,logs-workplace_search.*', + }); + }); +}); + +describe('setPreEightEnterpriseSearchIndicesReadOnly', () => { + it('does not rollover datastreams if there are none', async () => { + const getIndicesMock = jest.fn(getMockIndicesFxn(testIndicesWithoutDatastream)); + const getDatastreamsMock = jest.fn(() => Promise.resolve({ data_streams: [] })); + const rolloverMock = jest.fn(() => Promise.resolve(true)); + const addBlockMock = jest.fn(() => Promise.resolve({ acknowledged: true })); + const esClientMock = { + indices: { + get: getIndicesMock, + getDataStream: getDatastreamsMock, + rollover: rolloverMock, + addBlock: addBlockMock, + }, + } as unknown as ElasticsearchClient; + + const result = await setPreEightEnterpriseSearchIndicesReadOnly(esClientMock); + expect(result).toEqual(''); + expect(getIndicesMock).toHaveBeenCalledTimes(1); + expect(rolloverMock).not.toHaveBeenCalled(); + expect(addBlockMock).toHaveBeenCalledTimes(1); + }); + + it('does rollover datastreams if there are any', async () => { + const getIndicesMock = jest.fn(); + getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testIndices)); + getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testBackingIndex)); + getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testIndices)); + getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testBackingIndex)); + + const getDatastreamsMock = getMockDatastreamsFxn(additionalDatastreams); + const rolloverMock = jest.fn(() => Promise.resolve(true)); + const addBlockMock = jest.fn(() => Promise.resolve({ acknowledged: true })); + const esClientMock = { + indices: { + get: getIndicesMock, + getDataStream: getDatastreamsMock, + rollover: rolloverMock, + addBlock: addBlockMock, + }, + } as unknown as ElasticsearchClient; + + const result = await setPreEightEnterpriseSearchIndicesReadOnly(esClientMock); + expect(result).toEqual(''); + expect(getIndicesMock).toHaveBeenCalledTimes(4); + expect(rolloverMock).toHaveBeenCalledTimes(3); + expect(addBlockMock).toHaveBeenCalledTimes(5); + }); +}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/pre_eight_index_deprecator.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/pre_eight_index_deprecator.ts new file mode 100644 index 0000000000000..372815dd99c89 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/enterprise_search/pre_eight_index_deprecator.ts @@ -0,0 +1,141 @@ +/* + * 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 } from '@elastic/elasticsearch/lib/api/types'; +import { ElasticsearchClient } from '@kbn/core/server'; + +export const ENT_SEARCH_INDEX_PREFIX = '.ent-search-'; +export const ENT_SEARCH_DATASTREAM_PREFIXES = [ + 'logs-enterprise_search.', + 'logs-app_search.', + 'logs-workplace_search.', +]; +export const ENT_SEARCH_DATASTREAM_PATTERN = [ + 'logs-enterprise_search.*', + 'logs-app_search.*', + 'logs-workplace_search.*', +]; + +export interface EnterpriseSearchIndexMapping { + name: string; + hasDatastream: boolean; + datastreams: string[]; +} + +function is7xIncompatibleIndex(indexData: IndicesIndexState): boolean { + const isReadOnly = indexData.settings?.index?.verified_read_only ?? 'false'; + return Boolean( + indexData.settings?.index?.version?.created?.startsWith('7') && isReadOnly !== 'true' + ); +} + +export async function getPreEightEnterpriseSearchIndices( + esClient: ElasticsearchClient +): Promise { + const entSearchIndices = await esClient.indices.get({ + index: `${ENT_SEARCH_INDEX_PREFIX}*`, + ignore_unavailable: true, + expand_wildcards: ['all', 'hidden'], + }); + + const returnIndices: EnterpriseSearchIndexMapping[] = []; + + for (const [index, indexData] of Object.entries(entSearchIndices)) { + if (is7xIncompatibleIndex(indexData)) { + const dataStreamName = indexData.data_stream; + returnIndices.push({ + name: index, + hasDatastream: dataStreamName ? true : false, + datastreams: [dataStreamName ?? ''], + }); + } + } + + const { data_streams: entSearchDatastreams } = await esClient.indices.getDataStream({ + name: ENT_SEARCH_DATASTREAM_PATTERN.join(','), + expand_wildcards: ['all', 'hidden'], + }); + + const dsIndices = new Set(); + entSearchDatastreams.forEach(({ indices: dsi }) => { + dsi.forEach(({ index_name: indexName }) => { + dsIndices.add(indexName); + }); + }); + + if (!dsIndices.size) return returnIndices; + + for (const returnIndex of returnIndices) { + if (dsIndices.has(returnIndex.name)) { + dsIndices.delete(returnIndex.name); + } + } + + if (!dsIndices.size) return returnIndices; + + const entSearchDsIndices = await esClient.indices.get({ + index: Array.from(dsIndices.values()), + ignore_unavailable: true, + }); + + for (const [index, indexData] of Object.entries(entSearchDsIndices)) { + if (is7xIncompatibleIndex(indexData)) { + const dataStreamName = indexData.data_stream; + returnIndices.push({ + name: index, + hasDatastream: dataStreamName ? true : false, + datastreams: [dataStreamName ?? ''], + }); + } + } + + return returnIndices; +} + +export async function setPreEightEnterpriseSearchIndicesReadOnly( + esClient: ElasticsearchClient +): Promise { + // get the indices again to ensure nothing's changed since the last check + let indices = await getPreEightEnterpriseSearchIndices(esClient); + + // rollover any datastreams first + const rolledOverDatastreams: { [id: string]: boolean } = {}; + for (const index of indices) { + if (index.hasDatastream) { + for (const datastream of index.datastreams) { + if (datastream.length === 0 || rolledOverDatastreams[datastream]) { + continue; + } + + const indexResponse = await esClient.indices.rollover({ alias: datastream }); + + if (!indexResponse) { + return `Could not roll over datastream: ${index.name}`; + } + + rolledOverDatastreams[datastream] = true; + } + } + } + + if (Object.keys(rolledOverDatastreams).length > 0) { + // we rolled over at least one datastream, + // get the indices again + indices = await getPreEightEnterpriseSearchIndices(esClient); + } + + for (const index of indices) { + const indexName = index.name; + const indexResponse = await esClient.indices.addBlock({ index: indexName, block: 'write' }); + + if (!indexResponse || indexResponse.acknowledged !== true) { + return `Could not set index read-only: ${indexName}`; + } + } + + return ''; +} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/__snapshots__/index.test.ts.snap b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/__snapshots__/index.test.ts.snap index 97a71f7189886..098164789fd18 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/__snapshots__/index.test.ts.snap +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/__snapshots__/index.test.ts.snap @@ -80,7 +80,12 @@ Object { }, Object { "correctiveAction": Object { - "blockerForReindexing": undefined, + "excludedActions": Array [], + "metadata": Object { + "isClosedIndex": false, + "isFrozenIndex": false, + "isInDataStream": false, + }, "type": "reindex", }, "details": "This index was created using version: 6.8.13", @@ -93,21 +98,29 @@ Object { }, Object { "correctiveAction": Object { - "blockerForReindexing": undefined, - "type": "reindex", + "metadata": Object { + "isClosedIndex": false, + "isFrozenIndex": true, + "isInDataStream": false, + }, + "type": "unfreeze", }, - "details": "This index has version: 7.17.28-8.0.0", - "frozen": true, + "details": "Frozen indices must be unfrozen before upgrading to version 9.0. (The legacy frozen indices feature no longer offers any advantages. You may consider cold or frozen tiers in place of frozen indices.)", "index": "frozen_index", "isCritical": true, - "message": "Old index with a compatibility version < 8.0", + "message": "Index [frozen_index] is a frozen index. The frozen indices feature is deprecated and will be removed in version 9.0.", "resolveDuringUpgrade": false, "type": "index_settings", - "url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/migrating-8.0.html#breaking-changes-8.0", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/frozen-indices.html", }, Object { "correctiveAction": Object { - "blockerForReindexing": "index-closed", + "excludedActions": Array [], + "metadata": Object { + "isClosedIndex": true, + "isFrozenIndex": false, + "isInDataStream": false, + }, "type": "reindex", }, "details": "This index was created using version: 6.8.13", @@ -175,7 +188,33 @@ Object { }, Object { "correctiveAction": Object { - "blockerForReindexing": undefined, + "excludedActions": Array [], + "metadata": Object { + "isClosedIndex": false, + "isFrozenIndex": false, + "isInDataStream": false, + }, + "transformIds": Array [ + "abc", + ], + "type": "reindex", + }, + "details": "This index has version: 7.17.25", + "index": "transforms_index", + "isCritical": true, + "message": "Old index with a compatibility version < 8.0", + "resolveDuringUpgrade": false, + "type": "index_settings", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", + }, + Object { + "correctiveAction": Object { + "excludedActions": Array [], + "metadata": Object { + "isClosedIndex": false, + "isFrozenIndex": false, + "isInDataStream": false, + }, "type": "reindex", }, "details": "This index has version: 7.17.25", @@ -187,7 +226,20 @@ Object { "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html", }, Object { - "correctiveAction": undefined, + "correctiveAction": Object { + "metadata": Object { + "excludedActions": Array [], + "ignoredIndicesRequiringUpgrade": Array [], + "ignoredIndicesRequiringUpgradeCount": 0, + "indicesRequiringUpgrade": Array [ + ".ds-some-backing-index-5-2024.11.07-000001", + ], + "indicesRequiringUpgradeCount": 1, + "reindexRequired": true, + "totalBackingIndices": 2, + }, + "type": "dataStream", + }, "details": "This data stream has backing indices that were created before Elasticsearch 8.0.0", "index": "my-v7-data-stream", "isCritical": true, @@ -217,7 +269,7 @@ Object { "url": "https://github.com/elastic/elasticsearch/pull/117172", }, ], - "totalCriticalDeprecations": 8, + "totalCriticalDeprecations": 9, "totalCriticalHealthIssues": 0, } `; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/get_corrective_actions.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/get_corrective_actions.ts index 0635150243542..da3937965a065 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/get_corrective_actions.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/get_corrective_actions.ts @@ -5,24 +5,31 @@ * 2.0. */ -import { EnrichedDeprecationInfo } from '../../../common/types'; +import type { CorrectiveAction } from '../../../common/types'; +import type { BaseDeprecation } from './migrations'; interface Action { action_type: 'remove_settings'; objects: string[]; } -interface Actions { +interface CommonActionMetadata { actions?: Action[]; } -interface MlActionMetadata { - actions?: Action[]; +interface MlActionMetadata extends CommonActionMetadata { snapshot_id: string; job_id: string; } -interface DataStreamActionMetadata { - actions?: Action[]; + +interface IndexActionMetadata extends CommonActionMetadata { + reindex_required: boolean; + transform_ids: string[]; + is_in_data_stream?: boolean; +} + +interface DataStreamActionMetadata extends CommonActionMetadata { + excludedActions?: Array<'readOnly' | 'reindex'>; total_backing_indices: number; reindex_required: boolean; @@ -35,33 +42,28 @@ interface DataStreamActionMetadata { ignored_indices_requiring_upgrade_count?: number; } -export type EsMetadata = Actions | MlActionMetadata | DataStreamActionMetadata; +export type EsMetadata = IndexActionMetadata | MlActionMetadata | DataStreamActionMetadata; -// TODO(jloleysens): Replace these regexes once this issue is addressed https://github.com/elastic/elasticsearch/issues/118062 -const ES_INDEX_MESSAGES_REQIURING_REINDEX = [ - /Index created before/, - /index with a compatibility version \ + Boolean(indexName) && message.includes(`Index [${indexName}] is a frozen index`); + +export const getCorrectiveAction = (deprecation: BaseDeprecation): CorrectiveAction | undefined => { + const { index, type, message, metadata } = deprecation; -export const getCorrectiveAction = ( - deprecationType: EnrichedDeprecationInfo['type'], - message: string, - metadata: EsMetadata, - indexName?: string -): EnrichedDeprecationInfo['correctiveAction'] => { const indexSettingDeprecation = metadata?.actions?.find( - (action) => action.action_type === 'remove_settings' && indexName + (action) => action.action_type === 'remove_settings' && index ); const clusterSettingDeprecation = metadata?.actions?.find( - (action) => action.action_type === 'remove_settings' && typeof indexName === 'undefined' - ); - const requiresReindexAction = ES_INDEX_MESSAGES_REQIURING_REINDEX.some((regexp) => - regexp.test(message) + (action) => action.action_type === 'remove_settings' && typeof index === 'undefined' ); + const requiresReindexAction = + (type === 'index_settings' || type === 'node_settings') && + (deprecation.metadata as IndexActionMetadata)?.reindex_required === true; + const requiresUnfreezeAction = isFrozenDeprecation(message, index); const requiresIndexSettingsAction = Boolean(indexSettingDeprecation); const requiresClusterSettingsAction = Boolean(clusterSettingDeprecation); const requiresMlAction = /[Mm]odel snapshot/.test(message); - const requiresDataStreamsAction = deprecationType === 'data_streams'; + const requiresDataStreamsAction = type === 'data_streams'; if (requiresDataStreamsAction) { const { @@ -97,8 +99,26 @@ export const getCorrectiveAction = ( } if (requiresReindexAction) { + const transformIds = (metadata as IndexActionMetadata)?.transform_ids; return { type: 'reindex', + ...(transformIds?.length ? { transformIds } : {}), + metadata: { + isClosedIndex: Boolean(deprecation.isClosedIndex), + isFrozenIndex: Boolean(deprecation.isFrozenIndex), + isInDataStream: Boolean((deprecation.metadata as IndexActionMetadata)?.is_in_data_stream), + }, + }; + } + + if (requiresUnfreezeAction) { + return { + type: 'unfreeze', + metadata: { + isClosedIndex: Boolean(deprecation.isClosedIndex), + isFrozenIndex: Boolean(deprecation.isFrozenIndex), + isInDataStream: Boolean((deprecation.metadata as IndexActionMetadata)?.is_in_data_stream), + }, }; } diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/health_indicators.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/health_indicators.test.ts index 3f16d0f9e94ce..e296f82fa8075 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/health_indicators.test.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/health_indicators.test.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { elasticsearchServiceMock, ScopedClusterClientMock } from '@kbn/core/server/mocks'; +import { elasticsearchServiceMock, type ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { getHealthIndicators } from './health_indicators'; import * as healthIndicatorsMock from '../__fixtures__/health_indicators'; describe('getHealthIndicators', () => { - let esClient: ScopedClusterClientMock; + let esClient: ElasticsearchClientMock; beforeEach(() => { - esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; }); it('returns empty array on green indicators', async () => { - esClient.asCurrentUser.healthReport.mockResponse({ + esClient.healthReport.mockResponse({ cluster_name: 'mock', indicators: { disk: healthIndicatorsMock.diskIndicatorGreen, @@ -30,7 +30,7 @@ describe('getHealthIndicators', () => { }); it('returns unknown indicators', async () => { - esClient.asCurrentUser.healthReport.mockResponse({ + esClient.healthReport.mockResponse({ cluster_name: 'mock', indicators: { disk: healthIndicatorsMock.diskIndicatorUnknown, @@ -48,7 +48,7 @@ describe('getHealthIndicators', () => { }); it('returns unhealthy shards_capacity indicator', async () => { - esClient.asCurrentUser.healthReport.mockResponse({ + esClient.healthReport.mockResponse({ cluster_name: 'mock', indicators: { disk: healthIndicatorsMock.diskIndicatorGreen, @@ -92,7 +92,7 @@ describe('getHealthIndicators', () => { }); it('returns unhealthy disk indicator', async () => { - esClient.asCurrentUser.healthReport.mockResponse({ + esClient.healthReport.mockResponse({ cluster_name: 'mock', indicators: { disk: healthIndicatorsMock.diskIndicatorRed, diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/health_indicators.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/health_indicators.ts index 41a54ab186e56..fdcaba67c5d9a 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/health_indicators.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/health_indicators.ts @@ -6,14 +6,14 @@ */ import { i18n } from '@kbn/i18n'; -import type { estypes } from '@elastic/elasticsearch'; -import { IScopedClusterClient } from '@kbn/core/server'; -import { EnrichedDeprecationInfo } from '../../../common/types'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import type { EnrichedDeprecationInfo } from '../../../common/types'; export async function getHealthIndicators( - dataClient: IScopedClusterClient + dataClient: ElasticsearchClient ): Promise { - const healthIndicators = await dataClient.asCurrentUser.healthReport(); + const healthIndicators = await dataClient.healthReport(); const isStatusNotGreen = (indicator?: estypes.HealthReportBaseIndicator): boolean => { return !!(indicator?.status && indicator?.status !== 'green'); }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/index.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/index.test.ts index f3b9a27c68d1d..abbc29f619d4b 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/index.test.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/index.test.ts @@ -7,12 +7,12 @@ import _ from 'lodash'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; -import type { estypes } from '@elastic/elasticsearch'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import fakeDeprecations from '../__fixtures__/fake_deprecations.json'; import * as healthIndicatorsMock from '../__fixtures__/health_indicators'; import * as esMigrationsMock from '../__fixtures__/es_deprecations'; -import type { FeatureSet } from '../../../common/types'; +import type { DataSourceExclusions, FeatureSet } from '../../../common/types'; import { getESUpgradeStatus } from '.'; import { MigrationDeprecationsResponse } from '@elastic/elasticsearch/lib/api/types'; const fakeIndexNames = Object.keys(fakeDeprecations.index_settings); @@ -24,6 +24,7 @@ describe('getESUpgradeStatus', () => { mlSnapshots: true, migrateDataStreams: true, }; + const dataSourceExclusions: DataSourceExclusions = {}; const resolvedIndices = { indices: fakeIndexNames.map((indexName) => { @@ -38,13 +39,13 @@ describe('getESUpgradeStatus', () => { // @ts-expect-error mock data is too loosely typed const deprecationsResponse: estypes.MigrationDeprecationsResponse = _.cloneDeep(fakeDeprecations); - const esClient = elasticsearchServiceMock.createScopedClusterClient(); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; - esClient.asCurrentUser.healthReport.mockResponse({ cluster_name: 'mock', indicators: {} }); + esClient.healthReport.mockResponse({ cluster_name: 'mock', indicators: {} }); - esClient.asCurrentUser.migration.deprecations.mockResponse(deprecationsResponse); + esClient.migration.deprecations.mockResponse(deprecationsResponse); - esClient.asCurrentUser.transport.request.mockResolvedValue({ + esClient.transport.request.mockResolvedValue({ features: [ { feature_name: 'machine_learning', @@ -62,20 +63,20 @@ describe('getESUpgradeStatus', () => { }); // @ts-expect-error not full interface of response - esClient.asCurrentUser.indices.resolveIndex.mockResponse(resolvedIndices); + esClient.indices.resolveIndex.mockResponse(resolvedIndices); it('calls /_migration/deprecations', async () => { - await getESUpgradeStatus(esClient, featureSet); - expect(esClient.asCurrentUser.migration.deprecations).toHaveBeenCalled(); + await getESUpgradeStatus(esClient, { featureSet, dataSourceExclusions }); + expect(esClient.migration.deprecations).toHaveBeenCalled(); }); it('returns the correct shape of data', async () => { - const resp = await getESUpgradeStatus(esClient, featureSet); + const resp = await getESUpgradeStatus(esClient, { featureSet, dataSourceExclusions }); expect(resp).toMatchSnapshot(); }); it('returns totalCriticalDeprecations > 0 when critical issues found', async () => { - esClient.asCurrentUser.migration.deprecations.mockResponse({ + esClient.migration.deprecations.mockResponse({ // @ts-expect-error not full interface cluster_settings: [{ level: 'critical', message: 'Do count me', url: 'https://...' }], node_settings: [], @@ -86,14 +87,13 @@ describe('getESUpgradeStatus', () => { templates: {}, }); - await expect(getESUpgradeStatus(esClient, featureSet)).resolves.toHaveProperty( - 'totalCriticalDeprecations', - 1 - ); + await expect( + getESUpgradeStatus(esClient, { featureSet, dataSourceExclusions }) + ).resolves.toHaveProperty('totalCriticalDeprecations', 1); }); it('returns totalCriticalDeprecations === 0 when no critical issues found', async () => { - esClient.asCurrentUser.migration.deprecations.mockResponse({ + esClient.migration.deprecations.mockResponse({ // @ts-expect-error not full interface cluster_settings: [{ level: 'warning', message: 'Do not count me', url: 'https://...' }], node_settings: [], @@ -104,14 +104,13 @@ describe('getESUpgradeStatus', () => { templates: {}, }); - await expect(getESUpgradeStatus(esClient, featureSet)).resolves.toHaveProperty( - 'totalCriticalDeprecations', - 0 - ); + await expect( + getESUpgradeStatus(esClient, { featureSet, dataSourceExclusions }) + ).resolves.toHaveProperty('totalCriticalDeprecations', 0); }); it('filters out system indices returned by upgrade system indices API', async () => { - esClient.asCurrentUser.migration.deprecations.mockResponse({ + esClient.migration.deprecations.mockResponse({ cluster_settings: [], node_settings: [], ml_settings: [], @@ -132,7 +131,7 @@ describe('getESUpgradeStatus', () => { templates: {}, }); - const upgradeStatus = await getESUpgradeStatus(esClient, featureSet); + const upgradeStatus = await getESUpgradeStatus(esClient, { featureSet, dataSourceExclusions }); const { totalCriticalDeprecations, migrationsDeprecations, @@ -150,9 +149,12 @@ describe('getESUpgradeStatus', () => { ...esMigrationsMock.getMockMlSettingsDeprecations(), }; // @ts-ignore missing property definitions in ES resolve_during_rolling_upgrade and _meta - esClient.asCurrentUser.migration.deprecations.mockResponse(mockResponse); + esClient.migration.deprecations.mockResponse(mockResponse); - const enabledUpgradeStatus = await getESUpgradeStatus(esClient, { ...featureSet }); + const enabledUpgradeStatus = await getESUpgradeStatus(esClient, { + featureSet, + dataSourceExclusions, + }); expect([ ...enabledUpgradeStatus.migrationsDeprecations, ...enabledUpgradeStatus.enrichedHealthIndicators, @@ -160,8 +162,11 @@ describe('getESUpgradeStatus', () => { expect(enabledUpgradeStatus.totalCriticalDeprecations).toBe(1); const disabledUpgradeStatus = await getESUpgradeStatus(esClient, { - ...featureSet, - mlSnapshots: false, + featureSet: { + ...featureSet, + mlSnapshots: false, + }, + dataSourceExclusions, }); expect([ @@ -176,9 +181,12 @@ describe('getESUpgradeStatus', () => { ...esMigrationsMock.getMockEsDeprecations(), ...esMigrationsMock.getMockDataStreamDeprecations(), } as MigrationDeprecationsResponse; - esClient.asCurrentUser.migration.deprecations.mockResponse(mockResponse); + esClient.migration.deprecations.mockResponse(mockResponse); - const enabledUpgradeStatus = await getESUpgradeStatus(esClient, { ...featureSet }); + const enabledUpgradeStatus = await getESUpgradeStatus(esClient, { + featureSet, + dataSourceExclusions, + }); expect([ ...enabledUpgradeStatus.migrationsDeprecations, ...enabledUpgradeStatus.enrichedHealthIndicators, @@ -186,8 +194,11 @@ describe('getESUpgradeStatus', () => { expect(enabledUpgradeStatus.totalCriticalDeprecations).toBe(1); const disabledUpgradeStatus = await getESUpgradeStatus(esClient, { - ...featureSet, - migrateDataStreams: false, + featureSet: { + ...featureSet, + migrateDataStreams: false, + }, + dataSourceExclusions, }); expect([ @@ -198,7 +209,7 @@ describe('getESUpgradeStatus', () => { }); it('filters out reindex corrective actions if featureSet.reindexCorrectiveActions is set to false', async () => { - esClient.asCurrentUser.migration.deprecations.mockResponse({ + esClient.migration.deprecations.mockResponse({ cluster_settings: [], node_settings: [ { @@ -208,6 +219,9 @@ describe('getESUpgradeStatus', () => { details: 'This index was created using version: 6.8.13', // @ts-ignore resolve_during_rolling_upgrade: false, + _meta: { + reindex_required: true, + }, }, { level: 'critical', @@ -216,6 +230,9 @@ describe('getESUpgradeStatus', () => { details: 'This index was created using version: 6.8.13', // @ts-ignore resolve_during_rolling_upgrade: false, + _meta: { + reindex_required: true, + }, }, ], ml_settings: [], @@ -227,8 +244,11 @@ describe('getESUpgradeStatus', () => { }); const upgradeStatus = await getESUpgradeStatus(esClient, { - ...featureSet, - reindexCorrectiveActions: false, + dataSourceExclusions, + featureSet: { + ...featureSet, + reindexCorrectiveActions: false, + }, }); expect([ @@ -238,8 +258,92 @@ describe('getESUpgradeStatus', () => { expect(upgradeStatus.totalCriticalDeprecations).toBe(0); }); + it('filters out old index deprecations enterprise search indices and data streams', async () => { + esClient.migration.deprecations.mockResponse({ + cluster_settings: [], + node_settings: [], + ml_settings: [], + index_settings: { + '.ent-search-1': [ + { + level: 'critical', + message: 'Old index with a compatibility version < 8.0', + url: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/migrating-8.0.html#breaking-changes-8.0', + details: 'This index has version: 7.17.28-8.0.0', + resolve_during_rolling_upgrade: false, + _meta: { reindex_required: true }, + }, + { + level: 'critical', + message: + 'Index [.ent-search-1] is a frozen index. The frozen indices feature is deprecated and will be removed in version 9.0.', + url: 'https://www.elastic.co/guide/en/elasticsearch/reference/master/frozen-indices.html', + details: + 'Frozen indices must be unfrozen before upgrading to version 9.0. (The legacy frozen indices feature no longer offers any advantages. You may consider cold or frozen tiers in place of frozen indices.)', + resolve_during_rolling_upgrade: false, + }, + ], + '.ent-search-2': [ + { + level: 'critical', + message: + 'Index [.ent-search-2] is a frozen index. The frozen indices feature is deprecated and will be removed in version 9.0.', + url: 'https://www.elastic.co/guide/en/elasticsearch/reference/master/frozen-indices.html', + details: + 'Frozen indices must be unfrozen before upgrading to version 9.0. (The legacy frozen indices feature no longer offers any advantages. You may consider cold or frozen tiers in place of frozen indices.)', + resolve_during_rolling_upgrade: false, + }, + ], + }, + data_streams: { + 'logs-workplace_search.test': [ + { + level: 'critical', + message: 'Old data stream with a compatibility version < 8.0', + url: 'https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html', + details: + 'This data stream has backing indices that were created before Elasticsearch 8.0.0', + resolve_during_rolling_upgrade: false, + _meta: { + indices_requiring_upgrade: ['.ds-some-backing-index-5-2024.11.07-000001'], + indices_requiring_upgrade_count: 1, + total_backing_indices: 2, + reindex_required: true, + }, + }, + ], + }, + // @ts-expect-error not in types yet + ilm_policies: {}, + templates: {}, + }); + + const upgradeStatus = await getESUpgradeStatus(esClient, { + featureSet, + dataSourceExclusions: {}, + }); + + expect(upgradeStatus.migrationsDeprecations).toHaveLength(2); + expect( + upgradeStatus.migrationsDeprecations.find( + (dep) => + dep.correctiveAction?.type === 'reindex' || dep.correctiveAction?.type === 'dataStream' + ) + ).toBeUndefined(); + + expect( + upgradeStatus.migrationsDeprecations.find((dep) => dep.index === '.ent-search-1') + ).toMatchObject({ + details: expect.stringContaining('Frozen indices'), + }); + expect( + upgradeStatus.migrationsDeprecations.find((dep) => dep.index === '.ent-search-2') + ).toMatchObject({ + details: expect.stringContaining('Frozen indices'), + }); + }); it('filters out frozen indices if old index deprecations exist for the same indices', async () => { - esClient.asCurrentUser.migration.deprecations.mockResponse({ + esClient.migration.deprecations.mockResponse({ cluster_settings: [], node_settings: [], ml_settings: [], @@ -271,10 +375,11 @@ describe('getESUpgradeStatus', () => { }); // @ts-expect-error not full interface of response - esClient.asCurrentUser.indices.resolveIndex.mockResponse(resolvedIndices); + esClient.indices.resolveIndex.mockResponse(resolvedIndices); const upgradeStatus = await getESUpgradeStatus(esClient, { - ...featureSet, + featureSet, + dataSourceExclusions: {}, }); expect([ @@ -285,7 +390,7 @@ describe('getESUpgradeStatus', () => { }); it('returns health indicators', async () => { - esClient.asCurrentUser.migration.deprecations.mockResponse({ + esClient.migration.deprecations.mockResponse({ cluster_settings: [], node_settings: [ { @@ -295,6 +400,9 @@ describe('getESUpgradeStatus', () => { details: 'This index was created using version: 6.8.13', // @ts-ignore resolve_during_rolling_upgrade: false, + _meta: { + reindex_required: true, + }, }, ], ml_settings: [], @@ -305,7 +413,7 @@ describe('getESUpgradeStatus', () => { templates: {}, }); - esClient.asCurrentUser.healthReport.mockResponse({ + esClient.healthReport.mockResponse({ cluster_name: 'mock', indicators: { disk: healthIndicatorsMock.diskIndicatorGreen, @@ -314,7 +422,7 @@ describe('getESUpgradeStatus', () => { }, }); - const upgradeStatus = await getESUpgradeStatus(esClient, featureSet); + const upgradeStatus = await getESUpgradeStatus(esClient, { featureSet, dataSourceExclusions }); expect(upgradeStatus.totalCriticalHealthIssues + upgradeStatus.totalCriticalDeprecations).toBe( 2 ); @@ -350,6 +458,12 @@ describe('getESUpgradeStatus', () => { }, Object { "correctiveAction": Object { + "excludedActions": Array [], + "metadata": Object { + "isClosedIndex": false, + "isFrozenIndex": false, + "isInDataStream": false, + }, "type": "reindex", }, "details": "This index was created using version: 6.8.13", diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/index.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/index.ts index ac052e92d1275..bb09c8aa45b34 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/index.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/index.ts @@ -5,21 +5,69 @@ * 2.0. */ -import { IScopedClusterClient } from '@kbn/core/server'; -import { EnrichedDeprecationInfo, ESUpgradeStatus, FeatureSet } from '../../../common/types'; +import { ElasticsearchClient } from '@kbn/core/server'; +import { + EnrichedDeprecationInfo, + ESUpgradeStatus, + FeatureSet, + DataSourceExclusions, + DataStreamsAction, + ReindexAction, +} from '../../../common/types'; import { getEnrichedDeprecations } from './migrations'; import { getHealthIndicators } from './health_indicators'; +import { matchExclusionPattern } from '../data_source_exclusions'; export async function getESUpgradeStatus( - dataClient: IScopedClusterClient, - featureSet: FeatureSet + dataClient: ElasticsearchClient, + { + featureSet, + dataSourceExclusions, + }: { featureSet: FeatureSet; dataSourceExclusions: DataSourceExclusions } ): Promise { const getCombinedDeprecations = async () => { const healthIndicators = await getHealthIndicators(dataClient); const enrichedDeprecations = await getEnrichedDeprecations(dataClient); - const toggledMigrationsDeprecations = enrichedDeprecations.filter( - ({ type, correctiveAction }) => { + const toggledMigrationsDeprecations = enrichedDeprecations + .map((deprecation) => { + const correctiveActionType = deprecation.correctiveAction?.type; + if (correctiveActionType === 'dataStream') { + const excludedActions = matchExclusionPattern(deprecation.index!, dataSourceExclusions); + (deprecation.correctiveAction as DataStreamsAction).metadata.excludedActions = + excludedActions; + } else if (correctiveActionType === 'reindex') { + const excludedActions = matchExclusionPattern(deprecation.index!, dataSourceExclusions); + (deprecation.correctiveAction as ReindexAction).excludedActions = excludedActions; + } + return deprecation; + }) + .filter(({ correctiveAction }) => { + const correctiveActionType = correctiveAction?.type; + switch (correctiveActionType) { + // Only show the deprecation if there are actions that are not excluded + // This only applies to data streams since normal reindexing shows a "delete" manual option. + case 'dataStream': { + const { excludedActions } = (correctiveAction as DataStreamsAction).metadata; + + // nothing exlcuded, keep the deprecation + if (!excludedActions || !excludedActions.length) { + return true; + } + + // if all actions are excluded, don't show the deprecation + const allActionsExcluded = + excludedActions.includes('readOnly') && excludedActions.includes('reindex'); + + return !allActionsExcluded; + } + case 'reindex': + default: { + return true; + } + } + }) + .filter(({ type, correctiveAction }) => { /** * This disables showing the ML deprecations in the UA if `featureSet.mlSnapshots` * is set to `false`. @@ -49,8 +97,7 @@ export async function getESUpgradeStatus( } return true; - } - ); + }); const enrichedHealthIndicators = healthIndicators.filter(({ status }) => { return status !== 'green'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/migrations.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/migrations.ts index d5f3c3d46ca6b..0e6326e7a77d2 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/migrations.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_deprecations_status/migrations.ts @@ -9,15 +9,20 @@ import type { MigrationDeprecationsResponse, MigrationDeprecationsDeprecation, } from '@elastic/elasticsearch/lib/api/types'; -import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; -import _ from 'lodash'; -import { EnrichedDeprecationInfo } from '../../../common/types'; +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { omit } from 'lodash'; +import type { CorrectiveAction, EnrichedDeprecationInfo } from '../../../common/types'; import { convertFeaturesToIndicesArray, getESSystemIndicesMigrationStatus, } from '../es_system_indices_migration'; -import { type EsMetadata, getCorrectiveAction } from './get_corrective_actions'; +import { + type EsMetadata, + getCorrectiveAction, + isFrozenDeprecation, +} from './get_corrective_actions'; import { esIndicesStateCheck } from '../es_indices_state_check'; +import { ENT_SEARCH_DATASTREAM_PREFIXES, ENT_SEARCH_INDEX_PREFIX } from '../enterprise_search'; /** * Remove once the these keys are added to the `MigrationDeprecationsResponse` type @@ -27,10 +32,24 @@ interface EsDeprecations extends MigrationDeprecationsResponse { ilm_policies: Record; } -const createBaseMigrationDeprecation = ( +export interface BaseDeprecation { + index?: string; + type: keyof EsDeprecations; + details?: string; + message: string; + url: string; + isCritical: boolean; + metadata?: EsMetadata; + resolveDuringUpgrade: boolean; + // these properties apply to index_settings deprecations only + isFrozenIndex?: boolean; + isClosedIndex?: boolean; +} + +const createBaseDeprecation = ( migrationDeprecation: MigrationDeprecationsDeprecation, { deprecationType, indexName }: { deprecationType: keyof EsDeprecations; indexName?: string } -) => { +): BaseDeprecation => { const { details, message, @@ -47,16 +66,16 @@ const createBaseMigrationDeprecation = ( message, url, isCritical: level === 'critical', - metadata, + metadata: metadata as EsMetadata, resolveDuringUpgrade, }; }; const normalizeEsResponse = (migrationsResponse: EsDeprecations) => { - const indexSettingsMigrations = Object.entries(migrationsResponse.index_settings).flatMap( + const indexSettingsDeprecations = Object.entries(migrationsResponse.index_settings).flatMap( ([indexName, migrationDeprecations]) => { return migrationDeprecations.flatMap((migrationDeprecation) => - createBaseMigrationDeprecation(migrationDeprecation, { + createBaseDeprecation(migrationDeprecation, { indexName, deprecationType: 'index_settings', }) @@ -64,10 +83,10 @@ const normalizeEsResponse = (migrationsResponse: EsDeprecations) => { } ); - const dataStreamsMigrations = Object.entries(migrationsResponse.data_streams).flatMap( + const dataStreamsDeprecations = Object.entries(migrationsResponse.data_streams).flatMap( ([indexName, dataStreamDeprecations]) => { return dataStreamDeprecations.flatMap((depractionData) => - createBaseMigrationDeprecation(depractionData, { + createBaseDeprecation(depractionData, { indexName, deprecationType: 'data_streams', }) @@ -75,10 +94,10 @@ const normalizeEsResponse = (migrationsResponse: EsDeprecations) => { } ); - const ilmPoliciesMigrations = Object.entries(migrationsResponse.ilm_policies).flatMap( + const ilmPoliciesDeprecations = Object.entries(migrationsResponse.ilm_policies).flatMap( ([indexName, ilmPolicyDeprecations]) => { return ilmPolicyDeprecations.flatMap((ilmPolicyData) => - createBaseMigrationDeprecation(ilmPolicyData, { + createBaseDeprecation(ilmPolicyData, { indexName, deprecationType: 'ilm_policies', }) @@ -86,10 +105,10 @@ const normalizeEsResponse = (migrationsResponse: EsDeprecations) => { } ); - const templatesMigrations = Object.entries(migrationsResponse.templates).flatMap( - ([indexName, templatesDeprecations]) => { - return templatesDeprecations.flatMap((templatesDataa) => - createBaseMigrationDeprecation(templatesDataa, { + const templatesDeprecations = Object.entries(migrationsResponse.templates).flatMap( + ([indexName, templateDeprecations]) => { + return templateDeprecations.flatMap((templateData) => + createBaseDeprecation(templateData, { indexName, deprecationType: 'templates', }) @@ -97,116 +116,149 @@ const normalizeEsResponse = (migrationsResponse: EsDeprecations) => { } ); - const mlSettingsMigrations = migrationsResponse.ml_settings.map((depractionData) => - createBaseMigrationDeprecation(depractionData, { deprecationType: 'ml_settings' }) + const mlSettingsDeprecations = migrationsResponse.ml_settings.map((depractionData) => + createBaseDeprecation(depractionData, { deprecationType: 'ml_settings' }) ); - const nodeSettingsMigrations = migrationsResponse.node_settings.map((depractionData) => - createBaseMigrationDeprecation(depractionData, { deprecationType: 'node_settings' }) + const nodeSettingsDeprecations = migrationsResponse.node_settings.map((depractionData) => + createBaseDeprecation(depractionData, { deprecationType: 'node_settings' }) ); - const clusterSettingsMigrations = migrationsResponse.cluster_settings.map((depractionData) => - createBaseMigrationDeprecation(depractionData, { deprecationType: 'cluster_settings' }) + const clusterSettingsDeprecations = migrationsResponse.cluster_settings.map((depractionData) => + createBaseDeprecation(depractionData, { deprecationType: 'cluster_settings' }) ); return [ - ...clusterSettingsMigrations, - ...mlSettingsMigrations, - ...nodeSettingsMigrations, - ...indexSettingsMigrations, - ...dataStreamsMigrations, - ...ilmPoliciesMigrations, - ...templatesMigrations, + ...clusterSettingsDeprecations, + ...mlSettingsDeprecations, + ...nodeSettingsDeprecations, + ...indexSettingsDeprecations, + ...dataStreamsDeprecations, + ...ilmPoliciesDeprecations, + ...templatesDeprecations, ].flat(); }; +const isKnownDeprecation = (deprecation: BaseDeprecation): boolean => { + switch (deprecation.type) { + case 'index_settings': + case 'cluster_settings': + case 'templates': + case 'ilm_policies': + case 'ml_settings': + case 'node_settings': + case 'data_streams': { + return true; + } + default: { + return false; + } + } +}; + +const enrichIndexSettingsDeprecations = async ( + esClient: ElasticsearchClient, + deprecations: BaseDeprecation[] +): Promise => { + const deprecationsByIndex = new Map(); + const indexSettingsDeprecations = deprecations.filter( + (deprecation) => deprecation.type === 'index_settings' + ); + + // we do a first pass to store all the index deprecations in a Map + indexSettingsDeprecations.forEach((deprecation) => { + const indexDeprecations = deprecationsByIndex.get(deprecation.index!) ?? []; + indexDeprecations.push(deprecation); + deprecationsByIndex.set(deprecation.index!, indexDeprecations); + }); + + // fetch open/closed state for all of the index_settings deprecations indices + const indexNames = Array.from(deprecationsByIndex.keys()); + const indexStates = indexNames.length ? await esIndicesStateCheck(esClient, indexNames) : {}; + + // Update some properties for each of the index_settings deprecations + indexSettingsDeprecations.forEach((deprecation) => { + deprecation.isClosedIndex = indexStates[deprecation.index!] === 'closed'; + + // check if a given deprecation is a "frozen index deprecation" + const isFrozenIndex = isFrozenDeprecation(deprecation.message, deprecation.index); + + // update all deprecations for the same index + if (isFrozenIndex) { + deprecationsByIndex + .get(deprecation.index!)! + .forEach((indexDeprecation) => (indexDeprecation.isFrozenIndex = true)); + } + }); +}; + +const excludeDeprecation = ( + deprecation: BaseDeprecation, + correctiveAction?: CorrectiveAction +): boolean => { + if ( + deprecation.type === 'index_settings' && + correctiveAction?.type === 'reindex' && + deprecation.index?.startsWith(ENT_SEARCH_INDEX_PREFIX) + ) { + return true; + } else if ( + deprecation.type === 'data_streams' && + correctiveAction?.type === 'dataStream' && + correctiveAction.metadata.reindexRequired && + ENT_SEARCH_DATASTREAM_PREFIXES.some((prefix) => deprecation.index?.startsWith(prefix)) + ) { + return true; + } else if ( + deprecation.isCritical && + deprecation.type === 'index_settings' && + deprecation.isFrozenIndex && + correctiveAction?.type === 'reindex' + ) { + // in this scenario we will already have a "frozen index" deprecation for the same index + // we will filter this 'reindex' deprecation out, and let the 'unfreeze' one pass through + return true; + } + + return false; +}; + export const getEnrichedDeprecations = async ( - dataClient: IScopedClusterClient + esClient: ElasticsearchClient ): Promise => { - const deprecations = (await dataClient.asCurrentUser.migration.deprecations()) as EsDeprecations; - const systemIndices = await getESSystemIndicesMigrationStatus(dataClient.asCurrentUser); + const esDeprecations = (await esClient.migration.deprecations()) as EsDeprecations; + const deprecations = normalizeEsResponse(esDeprecations); + // Throwing here to avoid allowing upgrades while we have unhandled deprecation types from ES + // That might cause the stack to fail to start after upgrade. + deprecations.forEach((deprecation) => { + if (!isKnownDeprecation(deprecation)) { + throw new Error(`Unknown ES deprecation type "${deprecation.type}"`); + } + }); + + // Kibana system indices are handled in a different section of the Upgrade Assistant + const systemIndices = await getESSystemIndicesMigrationStatus(esClient); const systemIndicesList = convertFeaturesToIndicesArray(systemIndices.features); + const filteredDeprecations = deprecations.filter( + (deprecation) => + deprecation.type !== 'index_settings' || !systemIndicesList.includes(deprecation.index!) + ); - const indexSettingsIndexNames = Object.keys(deprecations.index_settings); - const indexSettingsIndexStates = indexSettingsIndexNames.length - ? await esIndicesStateCheck(dataClient.asCurrentUser, indexSettingsIndexNames) - : {}; - - const deprecationsByIndex = new Map(); - - return normalizeEsResponse(deprecations) - .filter((deprecation) => { - switch (deprecation.type) { - case 'index_settings': { - if (!deprecation.index) { - return false; - } - // filter out system indices - return !systemIndicesList.includes(deprecation.index); - } - case 'cluster_settings': - case 'templates': - case 'ilm_policies': - case 'ml_settings': - case 'node_settings': - case 'data_streams': { - return true; - } - default: { - // Throwing here to avoid allowing upgrades while we have unhandled deprecation types from ES - // That might cause the stack to fail to start after upgrade. - throw new Error(`Unknown ES deprecation type "${deprecation.type}"`); - } - } - }) - .map((deprecation) => { - const correctiveAction = getCorrectiveAction( - deprecation.type, - deprecation.message, - deprecation.metadata as EsMetadata, - deprecation.index - ); + // Set extra metadata properties for index_settings deprecations + await enrichIndexSettingsDeprecations(esClient, filteredDeprecations); - // If we have found deprecation information for index/indices - // check whether the index is open or closed. - if (deprecation.type === 'index_settings' && correctiveAction?.type === 'reindex') { - correctiveAction.blockerForReindexing = - indexSettingsIndexStates[deprecation.index!] === 'closed' ? 'index-closed' : undefined; - } - - const enrichedDeprecation = { - ..._.omit(deprecation, 'metadata'), - correctiveAction, - }; - - if (deprecation.index) { - const indexDeprecations = deprecationsByIndex.get(deprecation.index) || []; - indexDeprecations.push(enrichedDeprecation); - deprecationsByIndex.set(deprecation.index, indexDeprecations); - } - - return enrichedDeprecation; - }) - .filter((deprecation) => { - if ( - deprecation.index && - deprecation.message.includes(`Index [${deprecation.index}] is a frozen index`) - ) { - // frozen indices are created in 7.x, so they are old / incompatible as well - // reindexing + deleting is required, so no need to bubble up this deprecation in the UI - const indexDeprecations = deprecationsByIndex.get(deprecation.index)!; - const oldIndexDeprecation: EnrichedDeprecationInfo | undefined = indexDeprecations.find( - (elem) => - elem.type === 'index_settings' && - elem.index === deprecation.index && - elem.correctiveAction?.type === 'reindex' - ); - if (oldIndexDeprecation) { - oldIndexDeprecation.frozen = true; - return false; - } - } + // enrich deprecations with the corrective actions, remove metadata + return filteredDeprecations.flatMap((deprecation) => { + const correctiveAction = getCorrectiveAction(deprecation); - return true; - }); + // Prevent some deprecations from showing up in the UI + if (excludeDeprecation(deprecation, correctiveAction)) { + return []; // equivalent of filtering out, thanks to the flatMap + } + + return { + ...omit(deprecation, 'metadata', 'isFrozenIndex', 'isClosedIndex', 'isInDataStream'), + correctiveAction, + }; + }); }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_indices_state_check.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_indices_state_check.ts index deeb9310f9beb..ea18f8c5ce43a 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_indices_state_check.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/es_indices_state_check.ts @@ -12,19 +12,16 @@ import { ResolveIndexResponseFromES } from '../../common/types'; type StatusCheckResult = Record; export const esIndicesStateCheck = async ( - asCurrentUser: ElasticsearchClient, + esClient: ElasticsearchClient, indices: string[] ): Promise => { - const response = await asCurrentUser.indices.resolveIndex({ + const response = await esClient.indices.resolveIndex({ name: '*', expand_wildcards: 'all', }); - const result: StatusCheckResult = {}; - - indices.forEach((index) => { - result[index] = getIndexState(index, response as ResolveIndexResponseFromES); - }); - - return result; + return indices.reduce((acc, index) => { + acc[index] = getIndexState(index, response as ResolveIndexResponseFromES); + return acc; + }, {}); }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/index_settings.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/index_settings.ts index 178df91be6096..28955ab019cf8 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/index_settings.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/index_settings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ReindexWarning } from '../../../common/types'; +import { IndexWarning } from '../../../common/types'; import { versionService } from '../version'; import { FlatSettings } from './types'; export interface ParsedIndexName { @@ -65,7 +65,7 @@ export const generateNewIndexName = (indexName: string): string => { export const getDeprecatedSettingWarning = ( flatSettings: FlatSettings -): ReindexWarning | undefined => { +): IndexWarning | undefined => { const { settings } = flatSettings; const deprecatedSettingsInUse = Object.keys(settings || {}).filter((setting) => { @@ -88,6 +88,7 @@ export const getDeprecatedSettingWarning = ( if (deprecatedSettingsInUse.length) { return { + flow: 'all', warningType: 'indexSetting', meta: { deprecatedSettings: deprecatedSettingsInUse, @@ -100,8 +101,8 @@ export const getDeprecatedSettingWarning = ( * Returns an array of warnings that should be displayed to user before reindexing begins. * @param flatSettings */ -export const getReindexWarnings = (flatSettings: FlatSettings): ReindexWarning[] => { - const warnings = [] as ReindexWarning[]; +export const getReindexWarnings = (flatSettings: FlatSettings): IndexWarning[] => { + const warnings = [] as IndexWarning[]; if (versionService.getMajorVersion() === 8) { const deprecatedSettingWarning = getDeprecatedSettingWarning(flatSettings); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index 3ec5e8adab61c..4fba27d288e42 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -160,6 +160,11 @@ describe('reindexService', () => { const reindexWarnings = await service.detectReindexWarnings(indexName); expect(reindexWarnings).toEqual([ { + flow: 'readonly', + warningType: 'makeIndexReadonly', + }, + { + flow: 'reindex', warningType: 'replaceIndexWithAlias', }, ]); @@ -825,7 +830,7 @@ describe('reindexService', () => { ); it('moves existing aliases over to new index', async () => { - clusterClient.asCurrentUser.indices.getAlias.mockResponseOnce({ + clusterClient.asCurrentUser.indices.get.mockResponseOnce({ myIndex: { aliases: { myAlias: {}, diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 3c06ee56edd75..e05e0323b3ee8 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -10,11 +10,12 @@ import { firstValueFrom } from 'rxjs'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; +import { IndicesAlias, IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/types'; import { ReindexSavedObject, ReindexStatus, ReindexStep, - ReindexWarning, + IndexWarning, } from '../../../common/types'; import { esIndicesStateCheck } from '../es_indices_state_check'; @@ -37,7 +38,7 @@ export interface ReindexService { * Resolves to null if index does not exist. * @param indexName */ - detectReindexWarnings(indexName: string): Promise; + detectReindexWarnings(indexName: string): Promise; /** * Creates a new reindex operation for a given index. @@ -111,7 +112,15 @@ export interface ReindexService { */ cancelReindexing(indexName: string): Promise; - getIndexAliases(indexName: string): any; + /** + * Obtain metadata about the index, including aliases and settings + * @param indexName + */ + getIndexInfo(indexName: string): Promise<{ + aliases: Record; + settings?: IndicesIndexSettings; + isInDataStream: boolean; + }>; } export const reindexServiceFactory = ( @@ -163,11 +172,11 @@ export const reindexServiceFactory = ( const { indexName } = reindexOp.attributes; const putReadonly = await esClient.indices.putSettings({ index: indexName, - settings: { blocks: { write: true } }, + body: { blocks: { write: true } }, }); if (!putReadonly.acknowledged) { - throw new Error(`Index could not be set to readonly.`); + throw new Error(`Index could not be set to read-only.`); } return actions.updateReindexOp(reindexOp, { lastCompletedStep: ReindexStep.readonly }); @@ -341,12 +350,16 @@ export const reindexServiceFactory = ( return reindexOp; }; - const getIndexAliases = async (indexName: string) => { - const response = await esClient.indices.getAlias({ + const getIndexInfo = async (indexName: string) => { + const response = await esClient.indices.get({ index: indexName, + features: ['aliases', 'settings'], }); - return response[indexName]?.aliases ?? {}; + const aliases = response[indexName]?.aliases ?? {}; + const settings = response[indexName]?.settings?.index ?? {}; + const isInDataStream = Boolean(response[indexName]?.data_stream); + return { aliases, settings, isInDataStream }; }; const isIndexHidden = async (indexName: string) => { @@ -391,7 +404,7 @@ export const reindexServiceFactory = ( const switchAlias = async (reindexOp: ReindexSavedObject) => { const { indexName, newIndexName, reindexOptions } = reindexOp.attributes; - const existingAliases = await getIndexAliases(indexName); + const existingAliases = (await getIndexInfo(indexName)).aliases; const extraAliases = Object.keys(existingAliases).map((aliasName) => ({ add: { index: newIndexName, alias: aliasName, ...existingAliases[aliasName] }, @@ -466,9 +479,8 @@ export const reindexServiceFactory = ( return resp.has_all_requested; }, - async detectReindexWarnings(indexName: string): Promise { + async detectReindexWarnings(indexName: string): Promise { const flatSettings = await actions.getFlatSettings(indexName); - if (!flatSettings) { return undefined; } else { @@ -476,8 +488,13 @@ export const reindexServiceFactory = ( // By default all reindexing operations will replace an index with an alias (with the same name) // pointing to a newly created "reindexed" index. This is destructive as delete operations originally // done on the index itself will now need to be done to the "reindexed-{indexName}" + { + warningType: 'makeIndexReadonly', + flow: 'readonly', + }, { warningType: 'replaceIndexWithAlias', + flow: 'reindex', }, ...getReindexWarnings(flatSettings), ]; @@ -679,6 +696,6 @@ export const reindexServiceFactory = ( return reindexOp; }, - getIndexAliases, + getIndexInfo, }; }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/types.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/types.ts index d8446cea224cf..d046fa5f22f92 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/types.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/reindexing/types.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { estypes } from '@elastic/elasticsearch'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; interface Mapping { type?: string; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/lib/update_index/index.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/update_index/index.ts new file mode 100644 index 0000000000000..ec466dc544237 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/lib/update_index/index.ts @@ -0,0 +1,40 @@ +/* + * 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 type { ElasticsearchClient } from '@kbn/core/server'; +import type { UpdateIndexOperation } from '../../../common/update_index'; + +export interface UpdateIndexParams { + esClient: ElasticsearchClient; + index: string; + operations: UpdateIndexOperation[]; +} + +/** + * Perform some updates on a given index, to address compatibility issues. + * @param esClient Elasticsearch client, to issue http calls to ES + * @param index The index to update + * @param operations The operations to perform on the specified index + */ +export async function updateIndex({ esClient, index, operations }: UpdateIndexParams) { + for (const operation of operations) { + let res; + + switch (operation) { + case 'blockWrite': { + res = await esClient.indices.addBlock({ index, block: 'write' }); + break; + } + case 'unfreeze': { + throw new Error('Unfreeze is not supported after 8.x'); + } + } + if (!res.acknowledged) { + throw new Error(`Could not set apply ${operation} to ${index}.`); + } + } +} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/plugin.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/plugin.ts index 9641fd52ed324..6404ef828cb1c 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/plugin.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/plugin.ts @@ -37,7 +37,9 @@ import { import { handleEsError } from './shared_imports'; import { RouteDependencies } from './types'; import type { UpgradeAssistantConfig } from './config'; -import type { FeatureSet } from '../common/types'; +import type { DataSourceExclusions, FeatureSet } from '../common/types'; +import { getEntepriseSearchRegisteredDeprecations } from './lib/enterprise_search/enterprise_search_deprecations'; +import { defaultExclusions } from './lib/data_source_exclusions'; interface PluginsSetup { usageCollection: UsageCollectionSetup; @@ -56,6 +58,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { private readonly credentialStore: CredentialStore; private readonly kibanaVersion: string; private readonly initialFeatureSet: FeatureSet; + private readonly initialDataSourceExclusions: DataSourceExclusions; // Properties set at setup private licensing?: LicensingPluginSetup; @@ -70,8 +73,9 @@ export class UpgradeAssistantServerPlugin implements Plugin { this.credentialStore = credentialStoreFactory(this.logger); this.kibanaVersion = env.packageInfo.version; - const { featureSet } = config.get(); + const { featureSet, dataSourceExclusions } = config.get(); this.initialFeatureSet = featureSet; + this.initialDataSourceExclusions = Object.assign({}, defaultExclusions, dataSourceExclusions); } private getWorker() { @@ -82,7 +86,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { } setup( - { http, getStartServices, savedObjects }: CoreSetup, + { http, deprecations, getStartServices, savedObjects, docLinks }: CoreSetup, { usageCollection, features, licensing, logsShared, security }: PluginsSetup ) { this.licensing = licensing; @@ -139,6 +143,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { }, config: { featureSet: this.initialFeatureSet, + dataSourceExclusions: this.initialDataSourceExclusions, isSecurityEnabled: () => security !== undefined && security.license.isEnabled(), }, current: versionService.getCurrentVersion(), @@ -147,6 +152,11 @@ export class UpgradeAssistantServerPlugin implements Plugin { registerRoutes(dependencies, this.getWorker.bind(this)); + // Register deprecations for Enterprise Search pre-8 indices + deprecations.registerDeprecations({ + ...getEntepriseSearchRegisteredDeprecations(docLinks.links.enterpriseSearch.upgrade9x), + }); + if (usageCollection) { void getStartServices().then(([{ elasticsearch }]) => { registerUpgradeAssistantUsageCollector({ diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/cluster_settings.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/cluster_settings.ts index 29124b778e713..aae2b02267b93 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/cluster_settings.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/cluster_settings.ts @@ -65,8 +65,8 @@ export function registerClusterSettingsRoute({ ); const settingsResponse = await client.asCurrentUser.cluster.putSettings({ - ...settingsToDelete, flat_settings: true, + ...settingsToDelete, }); return response.ok({ diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/es_deprecations.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/es_deprecations.test.ts index 344e0ed76df25..a95f5ac984778 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/es_deprecations.test.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/es_deprecations.test.ts @@ -43,6 +43,7 @@ describe('ES deprecations API', () => { }, router: mockRouter, lib: { handleEsError }, + log: { error: jest.fn() }, }; registerESDeprecationRoutes(routeDependencies); }); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/es_deprecations.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/es_deprecations.ts index aa015c0523a18..132d1329faa25 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/es_deprecations.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/es_deprecations.ts @@ -8,12 +8,12 @@ import { API_BASE_PATH } from '../../common/constants'; import { getESUpgradeStatus } from '../lib/es_deprecations_status'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { RouteDependencies } from '../types'; +import type { RouteDependencies } from '../types'; import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; import { reindexServiceFactory } from '../lib/reindexing'; export function registerESDeprecationRoutes({ - config: { featureSet }, + config: { featureSet, dataSourceExclusions }, router, lib: { handleEsError }, licensing, @@ -36,8 +36,10 @@ export function registerESDeprecationRoutes({ savedObjects: { client: savedObjectsClient }, elasticsearch: { client }, } = await core; - const status = await getESUpgradeStatus(client, featureSet); - + const status = await getESUpgradeStatus(client.asCurrentUser, { + featureSet, + dataSourceExclusions, + }); const asCurrentUser = client.asCurrentUser; const reindexActions = reindexActionsFactory(savedObjectsClient, asCurrentUser); const reindexService = reindexServiceFactory(asCurrentUser, reindexActions, log, licensing); @@ -51,6 +53,7 @@ export function registerESDeprecationRoutes({ body: status, }); } catch (error) { + log.error(error); return handleEsError({ error, response }); } }) diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_data_streams/reindex_data_stream.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/migrate_data_streams/data_stream_routes.ts similarity index 54% rename from x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_data_streams/reindex_data_stream.ts rename to x-pack/platform/plugins/private/upgrade_assistant/server/routes/migrate_data_streams/data_stream_routes.ts index 2b2fb860d181a..8fa5c1a864639 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_data_streams/reindex_data_stream.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/migrate_data_streams/data_stream_routes.ts @@ -13,29 +13,81 @@ import { error } from '../../lib/data_streams/error'; import { API_BASE_PATH } from '../../../common/constants'; import { DataStreamReindexStatusResponse } from '../../../common/types'; import { versionCheckHandlerWrapper } from '../../lib/es_version_precheck'; -import { dataStreamReindexServiceFactory } from '../../lib/data_streams'; +import { dataStreamMigrationServiceFactory } from '../../lib/data_streams'; import { RouteDependencies } from '../../types'; import { mapAnyErrorToKibanaHttpResponse } from './map_any_error_to_kibana_http_response'; -export function registerReindexDataStreamRoutes({ +export function registerMigrateDataStreamRoutes({ router, licensing, log, - getSecurityPlugin, lib: { handleEsError }, }: RouteDependencies) { - const BASE_PATH = `${API_BASE_PATH}/reindex_data_streams`; + const BASE_PATH = `${API_BASE_PATH}/migrate_data_stream`; - router.post( + router.get( { path: `${BASE_PATH}/{dataStreamName}`, + options: { + access: 'public', + summary: `Get data stream status`, + }, + validate: { + params: schema.object({ + dataStreamName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper(async ({ core }, request, response) => { + const { + elasticsearch: { client: esClient }, + } = await core; + const { dataStreamName } = request.params; + const asCurrentUser = esClient.asCurrentUser; + + const migrationService = dataStreamMigrationServiceFactory({ + esClient: asCurrentUser, + log, + licensing, + }); + + try { + const hasRequiredPrivileges = await migrationService.hasRequiredPrivileges(dataStreamName); + const warnings = await migrationService.detectMigrationWarnings(dataStreamName); + const migrationOp = await migrationService.fetchMigrationStatus(dataStreamName); + + const body: DataStreamReindexStatusResponse = { + migrationOp, + warnings, + hasRequiredPrivileges, + }; + + return response.ok({ + body, + }); + } catch (err) { + if (err instanceof errors.ResponseError) { + return handleEsError({ error: err, response }); + } + return mapAnyErrorToKibanaHttpResponse(error); + } + }) + ); + + router.post( + { + path: `${BASE_PATH}/{dataStreamName}/reindex`, security: { authz: { enabled: false, reason: 'Relies on elasticsearch for authorization', }, }, + options: { + access: 'public', + summary: `Start the data stream reindexing`, + }, validate: { params: schema.object({ dataStreamName: schema.string(), @@ -49,25 +101,22 @@ export function registerReindexDataStreamRoutes({ const { dataStreamName } = request.params; try { const callAsCurrentUser = esClient.asCurrentUser; - const reindexService = dataStreamReindexServiceFactory({ + const migrationService = dataStreamMigrationServiceFactory({ esClient: callAsCurrentUser, log, licensing, }); - if (!(await reindexService.hasRequiredPrivileges(dataStreamName))) { + if (!(await migrationService.hasRequiredPrivileges(dataStreamName))) { throw error.accessForbidden( - i18n.translate( - 'xpack.upgradeAssistant.datastream.reindex.reindexPrivilegesErrorBatch', - { - defaultMessage: `You do not have adequate privileges to reindex "{dataStreamName}".`, - values: { dataStreamName }, - } - ) + i18n.translate('xpack.upgradeAssistant.datastream.reindexPrivilegesErrorBatch', { + defaultMessage: `You do not have adequate privileges to reindex "{dataStreamName}".`, + values: { dataStreamName }, + }) ); } - await reindexService.createReindexOperation(dataStreamName); + await migrationService.createReindexOperation(dataStreamName); return response.ok(); } catch (err) { @@ -81,12 +130,10 @@ export function registerReindexDataStreamRoutes({ router.get( { - path: `${BASE_PATH}/{dataStreamName}`, - security: { - authz: { - enabled: false, - reason: 'Relies on es client for authorization', - }, + path: `${BASE_PATH}/{dataStreamName}/metadata`, + options: { + access: 'public', + summary: `Get data stream metadata`, }, validate: { params: schema.object({ @@ -101,30 +148,17 @@ export function registerReindexDataStreamRoutes({ const { dataStreamName } = request.params; const asCurrentUser = esClient.asCurrentUser; - const reindexService = dataStreamReindexServiceFactory({ + const migrationService = dataStreamMigrationServiceFactory({ esClient: asCurrentUser, log, licensing, }); try { - const hasRequiredPrivileges = await reindexService.hasRequiredPrivileges(dataStreamName); - - // If the user doesn't have privileges than querying for warnings is going to fail. - const warnings = hasRequiredPrivileges - ? await reindexService.detectReindexWarnings(dataStreamName) - : []; - - const reindexOp = await reindexService.fetchReindexStatus(dataStreamName); - - const body: DataStreamReindexStatusResponse = { - reindexOp, - warnings, - hasRequiredPrivileges, - }; + const dataStreamMetadata = await migrationService.getDataStreamMetadata(dataStreamName); return response.ok({ - body, + body: dataStreamMetadata || undefined, }); } catch (err) { if (err instanceof errors.ResponseError) { @@ -135,15 +169,19 @@ export function registerReindexDataStreamRoutes({ }) ); - router.get( + router.post( { - path: `${BASE_PATH}/{dataStreamName}/metadata`, + path: `${BASE_PATH}/{dataStreamName}/reindex/cancel`, security: { authz: { enabled: false, - reason: 'Relies on es client for authorization', + reason: 'Relies on elasticsearch for authorization', }, }, + options: { + access: 'public', + summary: `Cancel Data Stream reindexing`, + }, validate: { params: schema.object({ dataStreamName: schema.string(), @@ -155,24 +193,32 @@ export function registerReindexDataStreamRoutes({ elasticsearch: { client: esClient }, } = await core; const { dataStreamName } = request.params; - const asCurrentUser = esClient.asCurrentUser; + const callAsCurrentUser = esClient.asCurrentUser; - const reindexService = dataStreamReindexServiceFactory({ - esClient: asCurrentUser, + const migrationService = dataStreamMigrationServiceFactory({ + esClient: callAsCurrentUser, log, licensing, }); try { - const dataStreamMetadata = await reindexService.getDataStreamMetadata(dataStreamName); + if (!(await migrationService.hasRequiredPrivileges(dataStreamName))) { + throw error.accessForbidden( + i18n.translate('xpack.upgradeAssistant.datastream.reindexPrivilegesErrorBatch', { + defaultMessage: `You do not have adequate privileges to cancel reindexing "{dataStreamName}".`, + values: { dataStreamName }, + }) + ); + } - return response.ok({ - body: dataStreamMetadata || undefined, - }); + await migrationService.cancelReindexing(dataStreamName); + + return response.ok({ body: { acknowledged: true } }); } catch (err) { if (err instanceof errors.ResponseError) { return handleEsError({ error: err, response }); } + return mapAnyErrorToKibanaHttpResponse(error); } }) @@ -180,14 +226,21 @@ export function registerReindexDataStreamRoutes({ router.post( { - path: `${BASE_PATH}/{dataStreamName}/cancel`, + path: `${BASE_PATH}/{dataStreamName}/readonly`, security: { authz: { enabled: false, reason: 'Relies on elasticsearch for authorization', }, }, + options: { + access: 'public', + summary: `Mark Data Stream indices as read only`, + }, validate: { + body: schema.object({ + indices: schema.arrayOf(schema.string()), + }), params: schema.object({ dataStreamName: schema.string(), }), @@ -198,36 +251,32 @@ export function registerReindexDataStreamRoutes({ elasticsearch: { client: esClient }, } = await core; const { dataStreamName } = request.params; + const { indices } = request.body; const callAsCurrentUser = esClient.asCurrentUser; - const reindexService = dataStreamReindexServiceFactory({ + const migrationService = dataStreamMigrationServiceFactory({ esClient: callAsCurrentUser, log, licensing, }); try { - if (!(await reindexService.hasRequiredPrivileges(dataStreamName))) { + if (!(await migrationService.hasRequiredPrivileges(dataStreamName))) { throw error.accessForbidden( - i18n.translate( - 'xpack.upgradeAssistant.datastream.reindex.reindexPrivilegesErrorBatch', - { - defaultMessage: `You do not have adequate privileges to cancel reindexing "{dataStreamName}".`, - values: { dataStreamName }, - } - ) + i18n.translate('xpack.upgradeAssistant.datastream.readonlyPrivilegesErrorBatch', { + defaultMessage: `You do not have adequate privileges to mark indices inside data stream as readonly "{dataStreamName}".`, + values: { dataStreamName }, + }) ); } - await reindexService.cancelReindexing(dataStreamName); - + await migrationService.readonlyIndices(dataStreamName, indices); return response.ok({ body: { acknowledged: true } }); } catch (err) { if (err instanceof errors.ResponseError) { return handleEsError({ error: err, response }); } - - return mapAnyErrorToKibanaHttpResponse(error); + return mapAnyErrorToKibanaHttpResponse(err); } }) ); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_data_streams/index.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/migrate_data_streams/index.ts similarity index 77% rename from x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_data_streams/index.ts rename to x-pack/platform/plugins/private/upgrade_assistant/server/routes/migrate_data_streams/index.ts index d96b3a19872b4..d5cc895702ddc 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_data_streams/index.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/migrate_data_streams/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { registerReindexDataStreamRoutes } from './reindex_data_stream'; +export { registerMigrateDataStreamRoutes } from './data_stream_routes'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_data_streams/map_any_error_to_kibana_http_response.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/migrate_data_streams/map_any_error_to_kibana_http_response.ts similarity index 87% rename from x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_data_streams/map_any_error_to_kibana_http_response.ts rename to x-pack/platform/plugins/private/upgrade_assistant/server/routes/migrate_data_streams/map_any_error_to_kibana_http_response.ts index d32ea749f4462..a719d8558ec99 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_data_streams/map_any_error_to_kibana_http_response.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/migrate_data_streams/map_any_error_to_kibana_http_response.ts @@ -14,16 +14,18 @@ import { ReindexCannotBeCancelled, ReindexTaskFailed, MetadataCannotBeGrabbed, + ReadonlyTaskFailed, } from '../../lib/data_streams/error_symbols'; -import { ReindexError } from '../../lib/data_streams/error'; +import { DataStreamMigrationError } from '../../lib/data_streams/error'; export const mapAnyErrorToKibanaHttpResponse = (e: any) => { - if (e instanceof ReindexError) { + if (e instanceof DataStreamMigrationError) { switch (e.symbol) { case AccessForbidden: return kibanaResponseFactory.forbidden({ body: e.message }); case IndexNotFound: return kibanaResponseFactory.notFound({ body: e.message }); + case ReadonlyTaskFailed: case ReindexTaskFailed: // Bad data return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/ml_snapshots.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/ml_snapshots.ts index ce1bb76b02735..feb52c2ddd9e7 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/ml_snapshots.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/ml_snapshots.ts @@ -345,12 +345,6 @@ export function registerMlSnapshotRoutes({ router.get( { path: `${API_BASE_PATH}/ml_upgrade_mode`, - security: { - authz: { - enabled: false, - reason: 'Relies on es client for authorization', - }, - }, validate: false, }, versionCheckHandlerWrapper(async ({ core }, request, response) => { @@ -393,12 +387,6 @@ export function registerMlSnapshotRoutes({ router.delete( { path: `${API_BASE_PATH}/ml_snapshots/{jobId}/{snapshotId}`, - security: { - authz: { - enabled: false, - reason: 'Relies on es client for authorization', - }, - }, validate: { params: schema.object({ snapshotId: schema.string(), diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/register_routes.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/register_routes.ts index 68b60418f5642..e65b665092588 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/register_routes.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/register_routes.ts @@ -21,7 +21,9 @@ import { registerUpgradeStatusRoute } from './status'; import { registerRemoteClustersRoute } from './remote_clusters'; import { registerNodeDiskSpaceRoute } from './node_disk_space'; import { registerClusterSettingsRoute } from './cluster_settings'; -import { registerReindexDataStreamRoutes } from './reindex_data_streams'; +import { registerMigrateDataStreamRoutes } from './migrate_data_streams'; +import { registerUpdateIndexRoute } from './update_index'; +import { registerEnterpriseSearchDeprecationRoutes } from '../lib/enterprise_search/enterprise_search_deprecations_routes'; export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { registerAppRoutes(dependencies); @@ -42,5 +44,11 @@ export function registerRoutes(dependencies: RouteDependencies, getWorker: () => registerClusterSettingsRoute(dependencies); // Data streams reindexing - registerReindexDataStreamRoutes(dependencies); + registerMigrateDataStreamRoutes(dependencies); + + // Mark index as read-only and unfreeze it + registerUpdateIndexRoute(dependencies); + + // Enterprise Search deprecations + registerEnterpriseSearchDeprecationRoutes(dependencies); } diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts index 5307299c366d1..31857ed979c07 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts @@ -42,6 +42,10 @@ export function registerBatchReindexIndicesRoutes( reason: 'Relies on es client for authorization', }, }, + options: { + access: 'public', + summary: `Get the batch reindex queue`, + }, validate: {}, }, versionCheckHandlerWrapper(async ({ core }, request, response) => { @@ -83,6 +87,10 @@ export function registerBatchReindexIndicesRoutes( reason: 'Relies on es client for authorization', }, }, + options: { + access: 'public', + summary: `Batch start or resume reindex`, + }, validate: { body: schema.object({ indexNames: schema.arrayOf(schema.string()), diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts index b02dd08f841d3..daf9daedf8b3a 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts @@ -24,6 +24,7 @@ const mockReindexService = { resumeReindexOperation: jest.fn(), cancelReindexing: jest.fn(), getIndexAliases: jest.fn().mockResolvedValue({}), + getIndexInfo: jest.fn().mockResolvedValue({ aliases: {}, settings: {} }), }; jest.mock('../../lib/es_version_precheck', () => ({ versionCheckHandlerWrapper: (a: any) => a, diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts index 6304cc3438570..0aeeaf64d7c90 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts @@ -40,6 +40,10 @@ export function registerReindexIndicesRoutes( reason: 'Relies on es and saved object clients for authorization', }, }, + options: { + access: 'public', + summary: `Start or resume reindex`, + }, validate: { params: schema.object({ indexName: schema.string(), @@ -83,11 +87,9 @@ export function registerReindexIndicesRoutes( router.get( { path: `${BASE_PATH}/{indexName}`, - security: { - authz: { - enabled: false, - reason: 'Relies on es and saved object clients for authorization', - }, + options: { + access: 'public', + summary: `Get reindex status`, }, validate: { params: schema.object({ @@ -117,7 +119,8 @@ export function registerReindexIndicesRoutes( ? await reindexService.detectReindexWarnings(indexName) : []; - const indexAliases = await reindexService.getIndexAliases(indexName); + const isTruthy = (value?: string | boolean): boolean => value === true || value === 'true'; + const { aliases, settings, isInDataStream } = await reindexService.getIndexInfo(indexName); const body: ReindexStatusResponse = { reindexOp: reindexOp ? reindexOp.attributes : undefined, @@ -126,7 +129,10 @@ export function registerReindexIndicesRoutes( meta: { indexName, reindexName: generateNewIndexName(indexName), - aliases: Object.keys(indexAliases), + aliases: Object.keys(aliases), + isFrozen: isTruthy(settings?.frozen), + isReadonly: isTruthy(settings?.verified_read_only), + isInDataStream, }, }; @@ -146,11 +152,9 @@ export function registerReindexIndicesRoutes( router.post( { path: `${BASE_PATH}/{indexName}/cancel`, - security: { - authz: { - enabled: false, - reason: 'Relies on es and saved object clients for authorization', - }, + options: { + access: 'public', + summary: `Cancel reindex`, }, validate: { params: schema.object({ diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/status.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/status.test.ts index ada9c4a16fdfa..4d516e634671e 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/status.test.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/status.test.ts @@ -118,6 +118,7 @@ describe('Status API', () => { lib: { handleEsError }, current: currentVersion, defaultTarget: nextMajor, + log: { error: jest.fn() }, }; registerUpgradeStatusRoute(routeDependencies); @@ -278,6 +279,7 @@ describe('Status API', () => { lib: { handleEsError }, current: currentVersion, defaultTarget: nextMajor, + log: { error: jest.fn() }, }; registerUpgradeStatusRoute(routeDependencies); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/status.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/status.ts index 54d00e0e8f132..333725140a643 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/status.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/status.ts @@ -18,11 +18,12 @@ import { getUpgradeType } from '../lib/upgrade_type'; * Note that this route is primarily intended for consumption by Cloud. */ export function registerUpgradeStatusRoute({ - config: { featureSet }, + config: { featureSet, dataSourceExclusions }, router, lib: { handleEsError }, current, defaultTarget, + log, }: RouteDependencies) { router.get( { @@ -57,7 +58,7 @@ export function registerUpgradeStatusRoute({ const { totalCriticalDeprecations, // critical deprecations totalCriticalHealthIssues, // critical health issues - } = await getESUpgradeStatus(esClient, featureSet); + } = await getESUpgradeStatus(esClient.asCurrentUser, { featureSet, dataSourceExclusions }); const getSystemIndicesMigrationStatus = async () => { /** @@ -159,6 +160,7 @@ export function registerUpgradeStatusRoute({ }, }); } catch (error) { + log.error(error); return handleEsError({ error, response }); } }) diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/system_indices_migration.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/system_indices_migration.ts index b28d9a62624fc..152703ef6c365 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/system_indices_migration.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/system_indices_migration.ts @@ -52,16 +52,7 @@ export function registerSystemIndicesMigrationRoutes({ // POST starts the system indices migration router.post( - { - path: `${API_BASE_PATH}/system_indices_migration`, - security: { - authz: { - enabled: false, - reason: 'Relies on es client for authorization', - }, - }, - validate: false, - }, + { path: `${API_BASE_PATH}/system_indices_migration`, validate: false }, versionCheckHandlerWrapper(async ({ core }, request, response) => { try { const { diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/update_index.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/update_index.ts new file mode 100644 index 0000000000000..46f30ead65dfb --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/update_index.ts @@ -0,0 +1,59 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { errors } from '@elastic/elasticsearch'; + +import { API_BASE_PATH } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import type { RouteDependencies } from '../types'; +import { updateIndex } from '../lib/update_index'; + +export function registerUpdateIndexRoute({ router, lib: { handleEsError } }: RouteDependencies) { + const BASE_PATH = `${API_BASE_PATH}/update_index`; + router.post( + { + path: `${BASE_PATH}/{index}`, + security: { + authz: { + enabled: false, + reason: 'Relies on es and saved object clients for authorization', + }, + }, + options: { + access: 'public', + summary: `Perform certain update operations on a given index. Currently supported ones are: 'blockWrite' and 'unfreeze'`, + }, + validate: { + params: schema.object({ + index: schema.string(), + }), + body: schema.object({ + operations: schema.arrayOf( + schema.oneOf([schema.literal('blockWrite'), schema.literal('unfreeze')]) + ), + }), + }, + }, + versionCheckHandlerWrapper(async ({ core }, request, response) => { + const { + elasticsearch: { client }, + } = await core; + const { index } = request.params; + const { operations } = request.body; + try { + await updateIndex({ esClient: client.asCurrentUser, index, operations }); + return response.ok(); + } catch (err) { + if (err instanceof errors.ResponseError) { + return handleEsError({ error: err, response }); + } + throw err; + } + }) + ); +} diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/update_index_settings.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/update_index_settings.ts index e05cf12c7cc69..f037381a9f52a 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/routes/update_index_settings.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/routes/update_index_settings.ts @@ -44,7 +44,7 @@ export function registerUpdateSettingsRoute({ router }: RouteDependencies) { const settingsResponse = await client.asCurrentUser.indices.putSettings({ index: indexName, - settings: settingsToDelete, + body: settingsToDelete, }); return response.ok({ diff --git a/x-pack/platform/plugins/private/upgrade_assistant/server/types.ts b/x-pack/platform/plugins/private/upgrade_assistant/server/types.ts index a5b1049bd6cb4..7210069f23df2 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/server/types.ts +++ b/x-pack/platform/plugins/private/upgrade_assistant/server/types.ts @@ -11,7 +11,7 @@ import { SecurityPluginStart } from '@kbn/security-plugin/server'; import SemVer from 'semver/classes/semver'; import { CredentialStore } from './lib/reindexing/credential_store'; import { handleEsError } from './shared_imports'; -import type { FeatureSet } from '../common/types'; +import type { DataSourceExclusions, FeatureSet } from '../common/types'; export interface RouteDependencies { router: IRouter; @@ -24,6 +24,7 @@ export interface RouteDependencies { handleEsError: typeof handleEsError; }; config: { + dataSourceExclusions: DataSourceExclusions; featureSet: FeatureSet; isSecurityEnabled: () => boolean; }; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/tsconfig.json b/x-pack/platform/plugins/private/upgrade_assistant/tsconfig.json index 57742b9b2320d..0fa473e380133 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/tsconfig.json +++ b/x-pack/platform/plugins/private/upgrade_assistant/tsconfig.json @@ -43,6 +43,8 @@ "@kbn/core-http-server-mocks", "@kbn/core-http-server-utils", "@kbn/core-elasticsearch-server", + "@kbn/core-deprecations-common", + "@kbn/core-deprecations-server" ], "exclude": [ "target/**/*", diff --git a/x-pack/test/upgrade_assistant_integration/config.ts b/x-pack/test/upgrade_assistant_integration/config.ts index 16ec47a06e40b..a60d053c5b3e9 100644 --- a/x-pack/test/upgrade_assistant_integration/config.ts +++ b/x-pack/test/upgrade_assistant_integration/config.ts @@ -6,11 +6,10 @@ */ import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; -import { ScoutTestRunConfigCategory } from '@kbn/scout-info'; -import { FtrConfigProviderContext } from '@kbn/test'; +import { FtrConfigProviderContext, EsVersion } from '@kbn/test'; import path from 'node:path'; -export default async function ({ readConfigFile }: FtrConfigProviderContext) { +export default async function ({ readConfigFile, log }: FtrConfigProviderContext) { // Read the Kibana API integration tests config file so that we can utilize its services. const kibanaAPITestsConfig = await readConfigFile( require.resolve('@kbn/test-suites-src/api_integration/config') @@ -19,8 +18,19 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../functional/config.base.js') ); + const esVersion = EsVersion.getDefault(); + const willRunEsv9 = esVersion.matchRange('9'); + + let esTestCluster = { + ...xPackFunctionalTestsConfig.get('esTestCluster'), + dataArchive: path.resolve(__dirname, './fixtures/data_archives/upgrade_assistant.zip'), + }; + if (willRunEsv9) { + log.info(`Detected ES version ${esVersion}; not loading data archive`); + esTestCluster = undefined; + } + return { - testConfigCategory: ScoutTestRunConfigCategory.API_TEST, testFiles: [require.resolve('./upgrade_assistant')], servers: xPackFunctionalTestsConfig.get('servers'), services: { @@ -38,9 +48,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--plugin-path=${path.resolve(__dirname, '../../../examples/developer_examples')}`, ], }, - esTestCluster: { - ...xPackFunctionalTestsConfig.get('esTestCluster'), - dataArchive: path.resolve(__dirname, './fixtures/data_archives/upgrade_assistant.zip'), - }, + esTestCluster, }; } diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/api_deprecations.ts b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/api_deprecations.ts index 2f6fbedf0b409..2be55e40d6fd7 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/api_deprecations.ts +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/api_deprecations.ts @@ -203,7 +203,7 @@ export default function ({ getService }: FtrProviderContext) { ); }); }); - it('GET /api/upgrade_assistant/status does not return { readyForUpgrade: false } if there are only critical API deprecations', async () => { + it('Readiness status excludes critical deprecations based on Kibana API usage', async () => { /** Throw in another critical deprecation... */ await supertest.get(`/api/routing_example/d/removed_route`).expect(200); // sleep a little until the usage counter is synced into ES @@ -220,7 +220,12 @@ export default function ({ getService }: FtrProviderContext) { 2000 ); const { body } = await supertest.get(`/api/upgrade_assistant/status`).expect(200); - expect(body.readyForUpgrade).to.be(true); + + // There are critical deprecations for Kibana API usage, but we do not + // surface them in readiness status + expect(body.readyForUpgrade).to.be(false); + expect(body.details?.length > 0).to.be(true); + expect(/Kibana/gi.test(body.details)).to.be(false); }); }); } diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.ts b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.ts index 8cfb94ae625e8..a2b6d51a86b34 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.ts +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.ts @@ -14,6 +14,7 @@ import { } from '@kbn/upgrade-assistant-plugin/common/types'; import { generateNewIndexName } from '@kbn/upgrade-assistant-plugin/server/lib/reindexing/index_settings'; import { getIndexState } from '@kbn/upgrade-assistant-plugin/common/get_index_state'; +import { sortBy } from 'lodash'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -39,6 +40,7 @@ export default function ({ getService }: FtrProviderContext) { }; describe('reindexing', function () { + this.onlyEsVersion('8'); // bail on first error in this suite since cases sequentially depend on each other this.bail(true); @@ -156,7 +158,7 @@ export default function ({ getService }: FtrProviderContext) { // This new index is the new soon to be created reindexed index. We create it // upfront to simulate a situation in which the user restarted kibana half // way through the reindex process and ended up with an extra index. - await es.indices.create({ index: 'reindexed-v9-dummydata' }); + await es.indices.create({ index: 'reindexed-v8-dummydata' }); const { body } = await supertest .post(`/api/upgrade_assistant/reindex/dummydata`) @@ -212,24 +214,27 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('shows no warnings', async () => { - const resp = await supertest.get(`/api/upgrade_assistant/reindex/reindexed-v8-6.0-data`); // reusing the index previously migrated in v8->v9 UA tests + it('shows reindex and read-only warnings', async () => { + const resp = await supertest.get(`/api/upgrade_assistant/reindex/reindexed-v7-6.0-data`); // reusing the index previously migrated in v7->v8 UA tests + expect(resp.body.warnings.length).to.be(2); // By default, all reindexing operations will replace an index with an alias (with the same name) // pointing to a newly created "reindexed" index. - expect(resp.body.warnings.length).to.be(1); - expect(resp.body.warnings[0].warningType).to.be('replaceIndexWithAlias'); + expect(sortBy(resp.body.warnings, 'warningType')).to.eql([ + { warningType: 'makeIndexReadonly', flow: 'readonly' }, + { warningType: 'replaceIndexWithAlias', flow: 'reindex' }, + ]); }); it('reindexes old 7.0 index', async () => { const { body } = await supertest - .post(`/api/upgrade_assistant/reindex/reindexed-v8-6.0-data`) // reusing the index previously migrated in v8->v9 UA tests + .post(`/api/upgrade_assistant/reindex/reindexed-v7-6.0-data`) // reusing the index previously migrated in v7->v8 UA tests .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.indexName).to.equal('reindexed-v8-6.0-data'); + expect(body.indexName).to.equal('reindexed-v7-6.0-data'); expect(body.status).to.equal(ReindexStatus.inProgress); - const lastState = await waitForReindexToComplete('reindexed-v8-6.0-data'); + const lastState = await waitForReindexToComplete('reindexed-v7-6.0-data'); expect(lastState.errorMessage).to.equal(null); expect(lastState.status).to.equal(ReindexStatus.completed); });