diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts index b8d8e3d2505ef..0726b8597de2e 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts @@ -26,7 +26,7 @@ const idToUrlMap = { const shareMock = sharePluginMock.createSetupContract(); shareMock.url.locators.get = (id) => ({ // @ts-expect-error This object is missing some properties that we're not using in the UI - getUrl: (): string | undefined => idToUrlMap[id], + useUrl: (): string | undefined => idToUrlMap[id], }); export const getAppContextMock = (mockHttpClient: HttpSetup) => ({ diff --git a/x-pack/plugins/upgrade_assistant/common/constants.ts b/x-pack/plugins/upgrade_assistant/common/constants.ts index 750d5283865c8..fa55771b003d4 100644 --- a/x-pack/plugins/upgrade_assistant/common/constants.ts +++ b/x-pack/plugins/upgrade_assistant/common/constants.ts @@ -24,9 +24,15 @@ export const indexSettingDeprecations = { export const API_BASE_PATH = '/api/upgrade_assistant'; +/** + * This is the repository where Cloud stores its backup snapshots. + */ +export const CLOUD_SNAPSHOT_REPOSITORY = 'found-snapshots'; + export const DEPRECATION_WARNING_UPPER_LIMIT = 999999; export const DEPRECATION_LOGS_SOURCE_ID = 'deprecation_logs'; export const DEPRECATION_LOGS_INDEX = '.logs-deprecation.elasticsearch-default'; export const DEPRECATION_LOGS_INDEX_PATTERN = '.logs-deprecation.elasticsearch-default'; +export const CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS = 60000; export const DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS = 60000; diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index a296e158481fa..7d0f3f49f2ee5 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -215,6 +215,11 @@ export interface EnrichedDeprecationInfo resolveDuringUpgrade: boolean; } +export interface CloudBackupStatus { + isBackedUp: boolean; + lastBackupTime?: string; +} + export interface ESUpgradeStatus { totalCriticalDeprecations: number; deprecations: EnrichedDeprecationInfo[]; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx index cb5518a66a1ec..0e595dcbad1d6 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx @@ -5,72 +5,40 @@ * 2.0. */ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import { useAppContext } from '../../../app_context'; - -const i18nTexts = { - backupStepTitle: i18n.translate('xpack.upgradeAssistant.overview.backupStepTitle', { - defaultMessage: 'Back up your data', - }), - - backupStepDescription: i18n.translate('xpack.upgradeAssistant.overview.backupStepDescription', { - defaultMessage: 'Back up your data before addressing any deprecation warnings.', - }), -}; - -const SnapshotRestoreAppLink: React.FunctionComponent = () => { - const { share } = useAppContext(); - - const [snapshotRestoreUrl, setSnapshotRestoreUrl] = useState(); - - useEffect(() => { - const getSnapshotRestoreUrl = async () => { - const locator = share.url.locators.get('SNAPSHOT_RESTORE_LOCATOR'); - - if (!locator) { - return; - } - - const url = await locator.getUrl({ - page: 'snapshots', - }); - setSnapshotRestoreUrl(url); +import type { CloudSetup } from '../../../../../../cloud/public'; +import { OnPremBackup } from './on_prem_backup'; +import { CloudBackup, CloudBackupStatusResponse } from './cloud_backup'; + +const title = i18n.translate('xpack.upgradeAssistant.overview.backupStepTitle', { + defaultMessage: 'Back up your data', +}); + +interface Props { + cloud?: CloudSetup; + cloudBackupStatusResponse?: CloudBackupStatusResponse; +} + +export const getBackupStep = ({ cloud, cloudBackupStatusResponse }: Props): EuiStepProps => { + if (cloud?.isCloudEnabled) { + return { + title, + status: cloudBackupStatusResponse!.data?.isBackedUp ? 'complete' : 'incomplete', + children: ( + + ), }; + } - getSnapshotRestoreUrl(); - }, [share]); - - return ( - - {i18n.translate('xpack.upgradeAssistant.overview.snapshotRestoreLink', { - defaultMessage: 'Create snapshot', - })} - - ); -}; - -const BackupStep: React.FunctionComponent = () => { - return ( - <> - -

{i18nTexts.backupStepDescription}

-
- - - - - - ); -}; - -export const getBackupStep = (): EuiStepProps => { return { - title: i18nTexts.backupStepTitle, + title, status: 'incomplete', - children: , + children: , }; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx new file mode 100644 index 0000000000000..2af9aa2e82702 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.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 React from 'react'; +import moment from 'moment-timezone'; +import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiLoadingContent, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiButton, + EuiSpacer, + EuiCallOut, +} from '@elastic/eui'; + +import { CloudBackupStatus } from '../../../../../common/types'; +import { UseRequestResponse } from '../../../../shared_imports'; +import { ResponseError } from '../../../lib/api'; + +export type CloudBackupStatusResponse = UseRequestResponse; + +interface Props { + cloudBackupStatusResponse: UseRequestResponse; + cloudSnapshotsUrl: string; +} + +export const CloudBackup: React.FunctionComponent = ({ + cloudBackupStatusResponse, + cloudSnapshotsUrl, +}) => { + const { isInitialRequest, isLoading, error, data, resendRequest } = cloudBackupStatusResponse; + + if (isInitialRequest && isLoading) { + return ; + } + + if (error) { + return ( + +

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

+ + {i18n.translate('xpack.upgradeAssistant.overview.cloudBackup.retryButton', { + defaultMessage: 'Try again', + })} + +
+ ); + } + + const lastBackupTime = moment(data!.lastBackupTime).toISOString(); + + const statusMessage = data!.isBackedUp ? ( + + + + + + + +

+ + {' '} + + + ), + }} + /> +

+
+
+
+ ) : ( + + + + + + + +

+ {i18n.translate('xpack.upgradeAssistant.overview.cloudBackup.noSnapshotMessage', { + defaultMessage: `Your data isn't backed up.`, + })} +

+
+
+
+ ); + + return ( + <> + {statusMessage} + + + + + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx new file mode 100644 index 0000000000000..7982a1248b289 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx @@ -0,0 +1,48 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; + +import { useAppContext } from '../../../app_context'; + +const SnapshotRestoreAppLink: React.FunctionComponent = () => { + const { share } = useAppContext(); + + const snapshotRestoreUrl = share.url.locators + .get('SNAPSHOT_RESTORE_LOCATOR') + ?.useUrl({ page: 'snapshots' }); + + return ( + + + + ); +}; + +export const OnPremBackup: React.FunctionComponent = () => { + return ( + <> + +

+ {i18n.translate('xpack.upgradeAssistant.overview.backupStepDescription', { + defaultMessage: 'Back up your data before addressing any deprecation issues.', + })} +

+
+ + + + + + ); +}; 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 cae6a6e263550..cd5f9a4c74343 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 @@ -20,6 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../shared_imports'; import { useAppContext } from '../../app_context'; import { getBackupStep } from './backup_step'; import { getFixIssuesStep } from './fix_issues_step'; @@ -27,6 +28,9 @@ import { getFixLogsStep } from './fix_logs_step'; import { getUpgradeStep } from './upgrade_step'; export const Overview: FunctionComponent = () => { + const { + services: { cloud }, + } = useKibana(); const { kibanaVersionInfo, breadcrumbs, docLinks, api } = useAppContext(); const { nextMajor } = kibanaVersionInfo; @@ -44,6 +48,12 @@ export const Overview: FunctionComponent = () => { breadcrumbs.setBreadcrumbs('overview'); }, [breadcrumbs]); + let cloudBackupStatusResponse; + + if (cloud?.isCloudEnabled) { + cloudBackupStatusResponse = api.useLoadCloudBackupStatus(); + } + return ( @@ -84,7 +94,7 @@ export const Overview: FunctionComponent = () => { ({ + path: `${API_BASE_PATH}/cloud_backup_status`, + method: 'get', + pollIntervalMs: CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS, + }); + } + public useLoadEsDeprecations() { return this.useRequest({ path: `${API_BASE_PATH}/es_deprecations`, diff --git a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts index e9c034117038a..0b606bba4fc55 100644 --- a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts +++ b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts @@ -14,6 +14,7 @@ export { SendRequestResponse, useRequest, UseRequestConfig, + UseRequestResponse, SectionLoading, GlobalFlyout, } from '../../../../src/plugins/es_ui_shared/public/'; diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index a9a2671a6c713..b47400c065bdd 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -20,6 +20,7 @@ import { InfraPluginSetup } from '../../infra/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; +import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX } from '../common/constants'; import { CredentialStore, credentialStoreFactory } from './lib/reindexing/credential_store'; import { ReindexWorker } from './lib/reindexing'; @@ -32,7 +33,7 @@ import { reindexOperationSavedObjectType, mlSavedObjectType, } from './saved_object_types'; -import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX } from '../common/constants'; +import { handleEsError } from './shared_imports'; import { RouteDependencies } from './types'; @@ -119,6 +120,9 @@ export class UpgradeAssistantServerPlugin implements Plugin { } return this.savedObjectsServiceStart; }, + lib: { + handleEsError, + }, }; // Initialize version service with current kibana version diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts b/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts new file mode 100644 index 0000000000000..5d3ab7c854e7b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts @@ -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 { API_BASE_PATH, CLOUD_SNAPSHOT_REPOSITORY } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerCloudBackupStatusRoutes({ + router, + lib: { handleEsError }, +}: RouteDependencies) { + // GET most recent Cloud snapshot + router.get( + { path: `${API_BASE_PATH}/cloud_backup_status`, validate: false }, + versionCheckHandlerWrapper(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; + + try { + const { + body: { snapshots }, + } = await clusterClient.asCurrentUser.snapshot.get({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + snapshot: '_all', + ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable. + // @ts-expect-error @elastic/elasticsearch "desc" is a new param + order: 'desc', + sort: 'start_time', + size: 1, + }); + + let isBackedUp = false; + let lastBackupTime; + + if (snapshots && snapshots[0]) { + isBackedUp = true; + lastBackupTime = snapshots![0].start_time; + } + + return response.ok({ + body: { + isBackedUp, + lastBackupTime, + }, + }); + } catch (error) { + return handleEsError({ error, response }); + } + }) + ); +} 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 332db10805692..813b25c4a79d0 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts @@ -7,6 +7,7 @@ import { RouteDependencies } from '../types'; +import { registerCloudBackupStatusRoutes } from './cloud_backup_status'; import { registerESDeprecationRoutes } from './es_deprecations'; import { registerDeprecationLoggingRoutes } from './deprecation_logging'; import { registerReindexIndicesRoutes } from './reindex_indices'; @@ -17,6 +18,7 @@ import { ReindexWorker } from '../lib/reindexing'; import { registerUpgradeStatusRoute } from './status'; export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { + registerCloudBackupStatusRoutes(dependencies); registerESDeprecationRoutes(dependencies); registerDeprecationLoggingRoutes(dependencies); registerReindexIndicesRoutes(dependencies, getWorker); diff --git a/x-pack/plugins/upgrade_assistant/server/types.ts b/x-pack/plugins/upgrade_assistant/server/types.ts index b25b73070e4cf..6c9ed3e517118 100644 --- a/x-pack/plugins/upgrade_assistant/server/types.ts +++ b/x-pack/plugins/upgrade_assistant/server/types.ts @@ -6,8 +6,9 @@ */ import { IRouter, Logger, SavedObjectsServiceStart } from 'src/core/server'; -import { CredentialStore } from './lib/reindexing/credential_store'; import { LicensingPluginSetup } from '../../licensing/server'; +import { CredentialStore } from './lib/reindexing/credential_store'; +import { handleEsError } from './shared_imports'; export interface RouteDependencies { router: IRouter; @@ -15,4 +16,7 @@ export interface RouteDependencies { log: Logger; getSavedObjectsService: () => SavedObjectsServiceStart; licensing: LicensingPluginSetup; + lib: { + handleEsError: typeof handleEsError; + }; }