diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts index 4f0e048da0792..b4ae504fb3b1a 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts @@ -158,6 +158,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadSystemIndicesMigrationStatus = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/system_indices_migration`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setLoadMlUpgradeModeResponse = (response?: object, error?: ResponseError) => { const status = error ? error.statusCode || 400 : 200; const body = error ? error : response; @@ -169,6 +180,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setSystemIndicesMigrationResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('POST', `${API_BASE_PATH}/system_indices_migration`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadCloudBackupStatusResponse, setLoadEsDeprecationsResponse, @@ -179,6 +201,8 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setDeleteMlSnapshotResponse, setUpgradeMlSnapshotStatusResponse, setLoadDeprecationLogsCountResponse, + setLoadSystemIndicesMigrationStatus, + setSystemIndicesMigrationResponse, setDeleteLogsCacheResponse, setStartReindexingResponse, setReindexStatusResponse, diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap new file mode 100644 index 0000000000000..2a512e8569d9f --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Overview - Migrate system indices - Flyout shows correct features in flyout table 1`] = ` +Array [ + Array [ + "Security", + "Migration failed", + ], + Array [ + "Machine Learning", + "Migration in progress", + ], + Array [ + "Kibana", + "Migration required", + ], + Array [ + "Logstash", + "Migration complete", + ], +] +`; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.test.ts new file mode 100644 index 0000000000000..1e74a966b3933 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { setupEnvironment } from '../../helpers'; +import { systemIndicesMigrationStatus } from './mocks'; + +describe('Overview - Migrate system indices - Flyout', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus(systemIndicesMigrationStatus); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + testBed.component.update(); + }); + + afterAll(() => { + server.restore(); + }); + + test('shows correct features in flyout table', async () => { + const { actions, table } = testBed; + + await actions.clickViewSystemIndicesState(); + + const { tableCellsValues } = table.getMetaData('flyoutDetails'); + + expect(tableCellsValues.length).toBe(systemIndicesMigrationStatus.features.length); + expect(tableCellsValues).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx new file mode 100644 index 0000000000000..c5f680319d0ab --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx @@ -0,0 +1,136 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from '../../helpers'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; + +describe('Overview - Migrate system indices', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeEach(async () => { + testBed = await setupOverviewPage(); + testBed.component.update(); + }); + + afterAll(() => { + server.restore(); + }); + + describe('Error state', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus(undefined, { + statusCode: 400, + message: 'error', + }); + + testBed = await setupOverviewPage(); + }); + + test('Is rendered', () => { + const { exists, component } = testBed; + component.update(); + + expect(exists('systemIndicesStatusErrorCallout')).toBe(true); + }); + + test('Lets the user attempt to reload migration status', async () => { + const { exists, component, actions } = testBed; + component.update(); + + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + upgrade_status: 'NO_UPGRADE_NEEDED', + }); + + await actions.clickRetrySystemIndicesButton(); + + expect(exists('noMigrationNeededSection')).toBe(true); + }); + }); + + test('No migration needed', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + upgrade_status: 'NO_UPGRADE_NEEDED', + }); + + testBed = await setupOverviewPage(); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('noMigrationNeededSection')).toBe(true); + expect(exists('startSystemIndicesMigrationButton')).toBe(false); + expect(exists('viewSystemIndicesStateButton')).toBe(false); + }); + + test('Migration in progress', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + upgrade_status: 'IN_PROGRESS', + }); + + testBed = await setupOverviewPage(); + + const { exists, component, find } = testBed; + + component.update(); + + // Start migration is disabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + expect(find('startSystemIndicesMigrationButton').props().disabled).toBe(true); + // But we keep view system indices CTA + expect(exists('viewSystemIndicesStateButton')).toBe(true); + }); + + describe('Migration needed', () => { + test('Initial state', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + upgrade_status: 'UPGRADE_NEEDED', + }); + + testBed = await setupOverviewPage(); + + const { exists, component, find } = testBed; + + component.update(); + + // Start migration should be enabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + expect(find('startSystemIndicesMigrationButton').props().disabled).toBe(false); + // Same for view system indices status + expect(exists('viewSystemIndicesStateButton')).toBe(true); + }); + + test('Handles errors when migrating', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + upgrade_status: 'UPGRADE_NEEDED', + }); + httpRequestsMockHelpers.setSystemIndicesMigrationResponse(undefined, { + statusCode: 400, + message: 'error', + }); + + testBed = await setupOverviewPage(); + + const { exists, component, find } = testBed; + + await act(async () => { + find('startSystemIndicesMigrationButton').simulate('click'); + }); + + component.update(); + + // Error is displayed + expect(exists('startSystemIndicesMigrationCalloutError')).toBe(true); + // CTA is enabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + expect(find('startSystemIndicesMigrationButton').props().disabled).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts new file mode 100644 index 0000000000000..298f537819507 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts @@ -0,0 +1,58 @@ +/* + * 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 { SystemIndicesMigrationStatus } from '../../../../common/types'; + +export const systemIndicesMigrationStatus: SystemIndicesMigrationStatus = { + upgrade_status: 'UPGRADE_NEEDED', + features: [ + { + feature_name: 'security', + minimum_index_version: '7.1.1', + upgrade_status: 'ERROR', + indices: [ + { + index: '.security-7', + version: '7.1.1', + }, + ], + }, + { + feature_name: 'machine_learning', + minimum_index_version: '7.1.2', + upgrade_status: 'IN_PROGRESS', + indices: [ + { + index: '.ml-config', + version: '7.1.2', + }, + ], + }, + { + feature_name: 'kibana', + minimum_index_version: '7.1.3', + upgrade_status: 'UPGRADE_NEEDED', + indices: [ + { + index: '.kibana', + version: '7.1.3', + }, + ], + }, + { + feature_name: 'logstash', + minimum_index_version: '7.1.4', + upgrade_status: 'NO_UPGRADE_NEEDED', + indices: [ + { + index: '.logstash-config', + version: '7.1.4', + }, + ], + }, + ], +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts new file mode 100644 index 0000000000000..c5173481d8dac --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { setupEnvironment, advanceTime } from '../../helpers'; +import { SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS } from '../../../../common/constants'; + +describe('Overview - Migrate system indices - Step completion', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + test(`It's complete when no upgrade is needed`, async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + upgrade_status: 'NO_UPGRADE_NEEDED', + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`migrateSystemIndicesStep-complete`)).toBe(true); + }); + + test(`It's incomplete when migration is needed`, async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + upgrade_status: 'UPGRADE_NEEDED', + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`migrateSystemIndicesStep-incomplete`)).toBe(true); + }); + + describe('Poll for new status', () => { + beforeEach(async () => { + jest.useFakeTimers(); + + // First request should make the step be incomplete + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + upgrade_status: 'IN_PROGRESS', + }); + + testBed = await setupOverviewPage(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('renders step as complete when a upgraded needed status is followed by a no upgrade needed', async () => { + const { exists } = testBed; + + expect(exists('migrateSystemIndicesStep-incomplete')).toBe(true); + + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + upgrade_status: 'NO_UPGRADE_NEEDED', + }); + + // Resolve the polling timeout. + await advanceTime(SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS); + testBed.component.update(); + + expect(exists('migrateSystemIndicesStep-complete')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts index 1457af010af5b..242d6893d1518 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts @@ -57,10 +57,32 @@ const createActions = (testBed: TestBed) => { component.update(); }; + const clickViewSystemIndicesState = async () => { + const { find, component } = testBed; + + await act(async () => { + find('viewSystemIndicesStateButton').simulate('click'); + }); + + component.update(); + }; + + const clickRetrySystemIndicesButton = async () => { + const { find, component } = testBed; + + await act(async () => { + find('systemIndicesStatusRetryButton').simulate('click'); + }); + + component.update(); + }; + return { clickDeprecationToggle, clickRetryButton, clickResetButton, + clickViewSystemIndicesState, + clickRetrySystemIndicesButton, }; }; diff --git a/x-pack/plugins/upgrade_assistant/common/constants.ts b/x-pack/plugins/upgrade_assistant/common/constants.ts index c037e2407ab04..db8e2a707baa8 100644 --- a/x-pack/plugins/upgrade_assistant/common/constants.ts +++ b/x-pack/plugins/upgrade_assistant/common/constants.ts @@ -36,3 +36,4 @@ export const DEPRECATION_LOGS_INDEX_PATTERN = '.logs-deprecation.elasticsearch-d export const CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS = 60000; export const DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS = 15000; +export const SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS = 15000; diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index f955aa05288e3..1189cfd1866c4 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -251,8 +251,8 @@ export interface DeprecationLoggingStatus { isDeprecationLoggingEnabled: boolean; } -export type UPGRADE_STATUS = 'UPGRADE_NEEDED' | 'NO_UPGRADE_NEEDED' | 'IN_PROGRESS'; -export interface SystemIndicesUpgradeFeature { +export type UPGRADE_STATUS = 'UPGRADE_NEEDED' | 'NO_UPGRADE_NEEDED' | 'IN_PROGRESS' | 'ERROR'; +export interface SystemIndicesMigrationFeature { id?: string; feature_name: string; minimum_index_version: string; @@ -262,7 +262,11 @@ export interface SystemIndicesUpgradeFeature { version: string; }>; } -export interface SystemIndicesUpgradeStatus { - features: SystemIndicesUpgradeFeature[]; +export interface SystemIndicesMigrationStatus { + features: SystemIndicesMigrationFeature[]; upgrade_status: UPGRADE_STATUS; } +export interface SystemIndicesMigrationStarted { + features: SystemIndicesMigrationFeature[]; + accepted: boolean; +} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx new file mode 100644 index 0000000000000..612fcdc2e4c03 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx @@ -0,0 +1,193 @@ +/* + * 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 { startCase } from 'lodash'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiLoadingSpinner, + EuiTitle, + EuiText, + EuiIcon, + EuiSpacer, + EuiInMemoryTable, +} from '@elastic/eui'; + +import { + SystemIndicesMigrationStatus, + SystemIndicesMigrationFeature, + UPGRADE_STATUS, +} from '../../../../../common/types'; + +export interface SystemIndicesFlyoutProps { + closeFlyout: () => void; + data: SystemIndicesMigrationStatus; + nextMajor: number; +} + +const i18nTexts = { + closeButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.flyoutCloseButtonLabel', + { + defaultMessage: 'Close', + } + ), + flyoutTitle: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.flyoutTitle', { + defaultMessage: 'Migrate system indices', + }), + flyoutDescription: (nextMajor: number) => + i18n.translate('xpack.upgradeAssistant.overview.systemIndices.flyoutDescription', { + defaultMessage: + 'Migrate the indices that store information for the following features before you upgrade to {nextMajor}.0.', + values: { nextMajor }, + }), + migrationCompleteLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.migrationCompleteLabel', + { + defaultMessage: 'Migration complete', + } + ), + needsMigrationLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.needsMigrationLabel', + { + defaultMessage: 'Migration required', + } + ), + migratingLabel: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.migratingLabel', { + defaultMessage: 'Migration in progress', + }), + errorLabel: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.errorLabel', { + defaultMessage: 'Migration failed', + }), + featureNameTableColumn: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.featureNameTableColumn', + { + defaultMessage: 'Feature', + } + ), + statusTableColumn: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.statusTableColumn', + { + defaultMessage: 'Status', + } + ), +}; + +const renderMigrationStatus = (status: UPGRADE_STATUS) => { + if (status === 'NO_UPGRADE_NEEDED') { + return ( + + + + + + +

{i18nTexts.migrationCompleteLabel}

+
+
+
+ ); + } + + if (status === 'UPGRADE_NEEDED') { + return ( + +

{i18nTexts.needsMigrationLabel}

+
+ ); + } + + if (status === 'IN_PROGRESS') { + return ( + + + + + + +

{i18nTexts.migratingLabel}

+
+
+
+ ); + } + + if (status === 'ERROR') { + return ( + + + + + + +

{i18nTexts.errorLabel}

+
+
+
+ ); + } + + return ''; +}; + +const columns = [ + { + field: 'feature_name', + name: i18nTexts.featureNameTableColumn, + sortable: true, + truncateText: true, + render: (name: string) => startCase(name), + }, + { + field: 'upgrade_status', + name: i18nTexts.statusTableColumn, + sortable: true, + render: renderMigrationStatus, + }, +]; + +export const SystemIndicesFlyout = ({ closeFlyout, data, nextMajor }: SystemIndicesFlyoutProps) => { + return ( + <> + + +

{i18nTexts.flyoutTitle}

+
+
+ + +

{i18nTexts.flyoutDescription(nextMajor)}

+
+ + + data-test-subj="featuresTable" + itemId="feature_name" + items={data.features} + columns={columns} + pagination={true} + sorting={true} + /> +
+ + + + + {i18nTexts.closeButtonLabel} + + + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/index.ts new file mode 100644 index 0000000000000..0be86929f2a43 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/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 { getMigrateSystemIndicesStep } from './migrate_system_indices'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx new file mode 100644 index 0000000000000..ab370c99bfc58 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx @@ -0,0 +1,197 @@ +/* + * 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, { FunctionComponent, useEffect } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiText, + EuiButton, + EuiSpacer, + EuiIcon, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import type { OverviewStepProps } from '../../types'; +import { useMigrateSystemIndices } from './use_migrate_system_indices'; + +interface Props { + setIsComplete: OverviewStepProps['setIsComplete']; +} + +interface StepProps extends OverviewStepProps { + nextMajor: number; +} + +const i18nTexts = { + title: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.title', { + defaultMessage: 'Migrate system indices', + }), + bodyDescription: (nextMajor: number) => ( + + ), + startButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.startButtonLabel', + { + defaultMessage: 'Migrate indices', + } + ), + inProgressButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.inProgressButtonLabel', + { + defaultMessage: 'Migration in progress', + } + ), + noMigrationNeeded: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.noMigrationNeeded', + { + defaultMessage: 'Migration complete', + } + ), + viewSystemIndicesStatus: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.viewSystemIndicesStatus', + { + defaultMessage: 'View migration information', + } + ), + retryButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.retryButtonLabel', + { + defaultMessage: 'Retry migration', + } + ), + loadingError: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.loadingError', { + defaultMessage: 'Could not retrieve the system indices status', + }), +}; + +const MigrateSystemIndicesStep: FunctionComponent = ({ setIsComplete }) => { + const { beginSystemIndicesMigration, startMigrationStatus, migrationStatus, setShowFlyout } = + useMigrateSystemIndices(); + + useEffect(() => { + setIsComplete(migrationStatus.data?.upgrade_status === 'NO_UPGRADE_NEEDED'); + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [migrationStatus.data?.upgrade_status]); + + if (migrationStatus.error) { + return ( + +

+ {migrationStatus.error.statusCode} - {migrationStatus.error.message} +

+ + {i18nTexts.retryButtonLabel} + +
+ ); + } + + if (migrationStatus.data?.upgrade_status === 'NO_UPGRADE_NEEDED') { + return ( + + + + + + +

{i18nTexts.noMigrationNeeded}

+
+
+
+ ); + } + + const isButtonDisabled = migrationStatus.isInitialRequest && migrationStatus.isLoading; + const isMigrating = migrationStatus.data?.upgrade_status === 'IN_PROGRESS'; + + return ( + <> + {startMigrationStatus.statusType === 'error' && ( + <> + + + + )} + + + + + {isMigrating ? i18nTexts.inProgressButtonLabel : i18nTexts.startButtonLabel} + + + + setShowFlyout(true)} + isDisabled={isButtonDisabled} + data-test-subj="viewSystemIndicesStateButton" + > + {i18nTexts.viewSystemIndicesStatus} + + + + + ); +}; + +export const getMigrateSystemIndicesStep = ({ + nextMajor, + isComplete, + setIsComplete, +}: StepProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + return { + title: i18nTexts.title, + status, + 'data-test-subj': `migrateSystemIndicesStep-${status}`, + children: ( + <> + +

{i18nTexts.bodyDescription(nextMajor)}

+
+ + + + + + ), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts new file mode 100644 index 0000000000000..d6e9bd2a9d5ff --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts @@ -0,0 +1,95 @@ +/* + * 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, useEffect } from 'react'; +import useInterval from 'react-use/lib/useInterval'; + +import { SystemIndicesFlyout, SystemIndicesFlyoutProps } from './flyout'; +import { useAppContext } from '../../../app_context'; +import type { ResponseError } from '../../../lib/api'; +import { GlobalFlyout } from '../../../../shared_imports'; +import { SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS } from '../../../../../common/constants'; + +const FLYOUT_ID = 'migrateSystemIndicesFlyout'; +const { useGlobalFlyout } = GlobalFlyout; + +export type StatusType = 'idle' | 'error' | 'started'; +interface MigrationStatus { + statusType: StatusType; + error?: ResponseError; +} + +export const useMigrateSystemIndices = () => { + const { + services: { api }, + kibanaVersionInfo: { nextMajor }, + } = useAppContext(); + + const [showFlyout, setShowFlyout] = useState(false); + + const [startMigrationStatus, setStartMigrationStatus] = useState({ + statusType: 'idle', + }); + + const { data, error, isLoading, resendRequest, isInitialRequest } = + api.useLoadSystemIndicesMigrationStatus(); + const isInProgress = data?.upgrade_status === 'IN_PROGRESS'; + + // We only want to poll for the status while the migration process is in progress. + useInterval(resendRequest, isInProgress ? SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS : null); + + const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = + useGlobalFlyout(); + + const closeFlyout = useCallback(() => { + setShowFlyout(false); + removeContentFromGlobalFlyout(FLYOUT_ID); + }, [removeContentFromGlobalFlyout]); + + useEffect(() => { + if (showFlyout) { + addContentToGlobalFlyout({ + id: FLYOUT_ID, + Component: SystemIndicesFlyout, + props: { + data: data!, + nextMajor, + closeFlyout, + }, + flyoutProps: { + onClose: closeFlyout, + }, + }); + } + }, [addContentToGlobalFlyout, data, showFlyout, closeFlyout, nextMajor]); + + const beginSystemIndicesMigration = useCallback(async () => { + const { error: startMigrationError } = await api.migrateSystemIndices(); + + setStartMigrationStatus({ + statusType: startMigrationError ? 'error' : 'started', + error: startMigrationError ?? undefined, + }); + + if (!startMigrationError) { + resendRequest(); + } + }, [api, resendRequest]); + + return { + setShowFlyout, + startMigrationStatus, + beginSystemIndicesMigration, + migrationStatus: { + data, + error, + isLoading, + resendRequest, + isInitialRequest, + }, + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx index 32e9c78f6d6ac..e382e3288bc68 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx @@ -25,8 +25,9 @@ import { getBackupStep } from './backup_step'; import { getFixIssuesStep } from './fix_issues_step'; import { getFixLogsStep } from './fix_logs_step'; import { getUpgradeStep } from './upgrade_step'; +import { getMigrateSystemIndicesStep } from './migrate_system_indices'; -type OverviewStep = 'backup' | 'fix_issues' | 'fix_logs'; +type OverviewStep = 'backup' | 'migrate_system_indices' | 'fix_issues' | 'fix_logs'; export const Overview: FunctionComponent = () => { const { @@ -55,6 +56,7 @@ export const Overview: FunctionComponent = () => { const [completedStepsMap, setCompletedStepsMap] = useState({ backup: false, + migrate_system_indices: false, fix_issues: false, fix_logs: false, }); @@ -112,6 +114,11 @@ export const Overview: FunctionComponent = () => { isComplete: isStepComplete('backup'), setIsComplete: setCompletedStep.bind(null, 'backup'), }), + getMigrateSystemIndicesStep({ + nextMajor, + isComplete: isStepComplete('migrate_system_indices'), + setIsComplete: setCompletedStep.bind(null, 'migrate_system_indices'), + }), getFixIssuesStep({ nextMajor, isComplete: isStepComplete('fix_issues'), @@ -121,7 +128,7 @@ export const Overview: FunctionComponent = () => { isComplete: isStepComplete('fix_logs'), setIsComplete: setCompletedStep.bind(null, 'fix_logs'), }), - getUpgradeStep({ docLinks, nextMajor }), + getUpgradeStep({ nextMajor }), ]} /> diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx index 5acbf280e0b4e..4af4a345be89d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx @@ -17,7 +17,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import type { DocLinksStart } from 'src/core/public'; import { useAppContext } from '../../../app_context'; const i18nTexts = { @@ -48,9 +47,12 @@ const i18nTexts = { }), }; -const UpgradeStep = ({ docLinks }: { docLinks: DocLinksStart }) => { +const UpgradeStep = () => { const { plugins: { cloud }, + services: { + core: { docLinks }, + }, } = useAppContext(); const isCloudEnabled: boolean = Boolean(cloud?.isCloudEnabled); let callToAction; @@ -115,16 +117,11 @@ const UpgradeStep = ({ docLinks }: { docLinks: DocLinksStart }) => { ); }; -interface Props { - docLinks: DocLinksStart; - nextMajor: number; -} - -export const getUpgradeStep = ({ docLinks, nextMajor }: Props): EuiStepProps => { +export const getUpgradeStep = ({ nextMajor }: { nextMajor: number }): EuiStepProps => { return { title: i18nTexts.upgradeStepTitle(nextMajor), status: 'incomplete', 'data-test-subj': 'upgradeStep', - children: , + children: , }; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts index b701a5a5eff2c..24a45fa1018fe 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts @@ -7,7 +7,11 @@ import { HttpSetup } from 'src/core/public'; -import { ESUpgradeStatus, CloudBackupStatus } from '../../../common/types'; +import { + ESUpgradeStatus, + CloudBackupStatus, + SystemIndicesMigrationStatus, +} from '../../../common/types'; import { API_BASE_PATH, DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS, @@ -58,6 +62,22 @@ export class ApiService { }); } + public useLoadSystemIndicesMigrationStatus() { + return this.useRequest({ + path: `${API_BASE_PATH}/system_indices_migration`, + method: 'get', + }); + } + + public async migrateSystemIndices() { + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/system_indices_migration`, + method: 'post', + }); + + return result; + } + public useLoadEsDeprecations() { return this.useRequest({ path: `${API_BASE_PATH}/es_deprecations`, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts index 401967d678a35..2e2c80b790cd5 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts @@ -12,9 +12,9 @@ import { EnrichedDeprecationInfo, ESUpgradeStatus } from '../../common/types'; import { esIndicesStateCheck } from './es_indices_state_check'; import { - getESSystemIndicesUpgradeStatus, + getESSystemIndicesMigrationStatus, convertFeaturesToIndicesArray, -} from '../lib/es_system_indices_upgrade'; +} from '../lib/es_system_indices_migration'; export async function getESUpgradeStatus( dataClient: IScopedClusterClient @@ -23,7 +23,7 @@ export async function getESUpgradeStatus( const getCombinedDeprecations = async () => { const indices = await getCombinedIndexInfos(deprecations, dataClient); - const systemIndices = await getESSystemIndicesUpgradeStatus(dataClient.asCurrentUser); + const systemIndices = await getESSystemIndicesMigrationStatus(dataClient.asCurrentUser); const systemIndicesList = convertFeaturesToIndicesArray(systemIndices.features); return Object.keys(deprecations).reduce((combinedDeprecations, deprecationType) => { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_upgrade.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts similarity index 89% rename from x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_upgrade.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts index a4a6e1053702c..6f4ca5a341218 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_upgrade.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { convertFeaturesToIndicesArray } from './es_system_indices_upgrade'; -import { SystemIndicesUpgradeStatus } from '../../common/types'; +import { convertFeaturesToIndicesArray } from './es_system_indices_migration'; +import { SystemIndicesMigrationStatus } from '../../common/types'; -const esUpgradeSystemIndicesStatusMock: SystemIndicesUpgradeStatus = { +const esUpgradeSystemIndicesStatusMock: SystemIndicesMigrationStatus = { features: [ { feature_name: 'machine_learning', diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_upgrade.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts similarity index 57% rename from x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_upgrade.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts index 32cea54a884e3..aa239de7dd008 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_upgrade.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts @@ -7,10 +7,14 @@ import { flow, flatMap, map, flatten, uniq } from 'lodash/fp'; import { ElasticsearchClient } from 'src/core/server'; -import { SystemIndicesUpgradeStatus, SystemIndicesUpgradeFeature } from '../../common/types'; +import { + SystemIndicesMigrationStatus, + SystemIndicesMigrationFeature, + SystemIndicesMigrationStarted, +} from '../../common/types'; export const convertFeaturesToIndicesArray = ( - features: SystemIndicesUpgradeFeature[] + features: SystemIndicesMigrationFeature[] ): string[] => { return flow( // Map each feature into Indices[] @@ -24,13 +28,24 @@ export const convertFeaturesToIndicesArray = ( )(features); }; -export const getESSystemIndicesUpgradeStatus = async ( +export const getESSystemIndicesMigrationStatus = async ( client: ElasticsearchClient -): Promise => { +): Promise => { const { body } = await client.transport.request({ method: 'GET', path: '/_migration/system_features', }); - return body as SystemIndicesUpgradeStatus; + return body as SystemIndicesMigrationStatus; +}; + +export const startESSystemIndicesMigration = async ( + client: ElasticsearchClient +): Promise => { + const { body } = await client.transport.request({ + method: 'POST', + path: '/_migration/system_features', + }); + + return body as SystemIndicesMigrationStarted; }; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts index cc4be54042583..ec02e814b2a8d 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts @@ -9,6 +9,7 @@ import { RouteDependencies } from '../types'; import { registerAppRoutes } from './app'; import { registerCloudBackupStatusRoutes } from './cloud_backup_status'; +import { registerSystemIndicesMigrationRoutes } from './system_indices_migration'; import { registerESDeprecationRoutes } from './es_deprecations'; import { registerDeprecationLoggingRoutes } from './deprecation_logging'; import { registerReindexIndicesRoutes, registerBatchReindexIndicesRoutes } from './reindex_indices'; @@ -21,6 +22,7 @@ import { registerUpgradeStatusRoute } from './status'; export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { registerAppRoutes(dependencies); registerCloudBackupStatusRoutes(dependencies); + registerSystemIndicesMigrationRoutes(dependencies); registerESDeprecationRoutes(dependencies); registerDeprecationLoggingRoutes(dependencies); registerReindexIndicesRoutes(dependencies, getWorker); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts new file mode 100644 index 0000000000000..2d15bed7e29e3 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts @@ -0,0 +1,132 @@ +/* + * 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 'src/core/server'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; +import { createRequestMock } from './__mocks__/request.mock'; +import { handleEsError } from '../shared_imports'; + +jest.mock('../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +import { registerSystemIndicesMigrationRoutes } from './system_indices_migration'; + +const mockedResponse = { + features: [ + { + feature_name: 'security', + minimum_index_version: '7.1.1', + upgrade_status: 'NO_UPGRADE_NEEDED', + indices: [ + { + index: '.security-7', + version: '7.1.1', + }, + ], + }, + ], + upgrade_status: 'UPGRADE_NEEDED', +}; + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the lib functions correctly. + */ +describe('Migrate system indices API', () => { + let mockRouter: MockRouter; + let routeDependencies: any; + + beforeEach(() => { + mockRouter = createMockRouter(); + routeDependencies = { + router: mockRouter, + lib: { handleEsError }, + }; + registerSystemIndicesMigrationRoutes(routeDependencies); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /api/upgrade_assistant/system_indices_migration', () => { + it('returns system indices migration status', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockResolvedValue({ + body: mockedResponse, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'GET', + path: '/_migration/system_features', + }); + expect(resp.payload).toEqual(mockedResponse); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); + + describe('POST /api/upgrade_assistant/system_indices_migration', () => { + it('returns system indices migration status', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockResolvedValue({ + body: mockedResponse, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'POST', + path: '/_migration/system_features', + }); + expect(resp.payload).toEqual(mockedResponse); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts new file mode 100644 index 0000000000000..dbb0cca7502bb --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts @@ -0,0 +1,71 @@ +/* + * 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 { API_BASE_PATH } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; +import { + getESSystemIndicesMigrationStatus, + startESSystemIndicesMigration, +} from '../lib/es_system_indices_migration'; + +export function registerSystemIndicesMigrationRoutes({ + router, + lib: { handleEsError }, +}: RouteDependencies) { + // GET status of the system indices migration + router.get( + { path: `${API_BASE_PATH}/system_indices_migration`, validate: false }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const status = await getESSystemIndicesMigrationStatus(client.asCurrentUser); + + return response.ok({ + body: status, + }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); + + // POST starts the system indices migration + router.post( + { path: `${API_BASE_PATH}/system_indices_migration`, validate: false }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const status = await startESSystemIndicesMigration(client.asCurrentUser); + + return response.ok({ + body: status, + }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); +}