[main] [Upgrade Assistant] Data streams reindexing (#208083) (#209029)

# Backport

This will backport the following commits from `8.x` to `main`:
- [[Upgrade Assistant] Data streams reindexing
(#208083)](https://github.com/elastic/kibana/pull/208083)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Ahmad
Bamieh","email":"ahmad.bamyeh@elastic.co"},"sourceCommit":{"committedDate":"2025-01-30T22:33:10Z","message":"[Upgrade
Assistant] Data streams reindexing (#208083)\n\n## Data streams
reindexing\r\n\r\nThis PR implements data stream reindexing corrective
action.\r\n\r\n\r\n## Screenshots\r\nFound
here\r\nhttps://docs.google.com/document/d/1QntGX5aTbjAv-VRZoKg43McZ_obddLkIPAQp_molMvw/edit?tab=t.0#heading=h.spoobki9vra3\r\n\r\n\r\n##
Testing\r\n#### Handling kibana restarts:\r\nSince we dont maintain any
state in Kibana for the data stream\r\nreindexing we dont worry about
this scenario\r\n\r\n#### Ensuring only one node is handling the task
sync with ES:\r\nSince we dont maintain any state in Kibana for the data
stream\r\nreindexing we dont worry about this scenario\r\n\r\n#### ES
task returns an exception:\r\nThis is a new state that was introduced in
persistent tasks. We show a\r\nhuge exception banner and ask user to
retry.\r\n\r\n#### ES task fails to reindex some indices but reindexing
is complete:\r\nShow an error in the in progress page. This is an
undesired state since\r\nthe user will still be asked to reindex the
data stream again for only\r\nthose failed indices.\r\n\r\n#### ES task
fails to reindex some indices but reindexing is still
in\r\nprogress\r\nWe should a count of how many failed while showing a
progress bar.\r\n\r\n#### Pause/resume/cancel functionality\r\ndata
transform reindexing only has ‘cancel’ from ES side however
it\r\nbehaves as “pause” since it will pick up where it last was
cancelled.\r\nSince we do not maintain a state in kibana we only show a
cancel button.\r\nOnce the reindex is cancelled the user can start it
again. All the\r\ndescriptions are updated from ES with the latest
incomplete countes.\r\n\r\n#### ES task stops responding\r\nWe mark the
Kibana task as failed\r\n\r\n#### ES task returns 404\r\nBoth cancelled
tasks and never started ones return 404 from ES side.\r\nThe only way to
tell the difference is to see if the Kibana task is “in\r\nprogress”
then this means it was cancelled from ES side (canelled via\r\nAPI by
the user directly for example) and we mark it as cancelled for\r\nthe
user.\r\n\r\n\r\n#### Other main flow tests:\r\n- Tested a small data
stream reindexing job\r\n- Tested a huge data stream reindexing job\r\n-
Tested pausing a job\r\n- Tested cancelling a job\r\n- Stopping ES
during reindexing and then restarting\r\n- Reindexing in a cluster with
2 Kibana
nodes","sha":"6925d129455ccb458e08cf33ce36c08e0d5313f5","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:prev-major","v8.18.0"],"title":"[Upgrade
Assistant] Data streams
reindexing","number":208083,"url":"https://github.com/elastic/kibana/pull/208083","mergeCommit":{"message":"[Upgrade
Assistant] Data streams reindexing (#208083)\n\n## Data streams
reindexing\r\n\r\nThis PR implements data stream reindexing corrective
action.\r\n\r\n\r\n## Screenshots\r\nFound
here\r\nhttps://docs.google.com/document/d/1QntGX5aTbjAv-VRZoKg43McZ_obddLkIPAQp_molMvw/edit?tab=t.0#heading=h.spoobki9vra3\r\n\r\n\r\n##
Testing\r\n#### Handling kibana restarts:\r\nSince we dont maintain any
state in Kibana for the data stream\r\nreindexing we dont worry about
this scenario\r\n\r\n#### Ensuring only one node is handling the task
sync with ES:\r\nSince we dont maintain any state in Kibana for the data
stream\r\nreindexing we dont worry about this scenario\r\n\r\n#### ES
task returns an exception:\r\nThis is a new state that was introduced in
persistent tasks. We show a\r\nhuge exception banner and ask user to
retry.\r\n\r\n#### ES task fails to reindex some indices but reindexing
is complete:\r\nShow an error in the in progress page. This is an
undesired state since\r\nthe user will still be asked to reindex the
data stream again for only\r\nthose failed indices.\r\n\r\n#### ES task
fails to reindex some indices but reindexing is still
in\r\nprogress\r\nWe should a count of how many failed while showing a
progress bar.\r\n\r\n#### Pause/resume/cancel functionality\r\ndata
transform reindexing only has ‘cancel’ from ES side however
it\r\nbehaves as “pause” since it will pick up where it last was
cancelled.\r\nSince we do not maintain a state in kibana we only show a
cancel button.\r\nOnce the reindex is cancelled the user can start it
again. All the\r\ndescriptions are updated from ES with the latest
incomplete countes.\r\n\r\n#### ES task stops responding\r\nWe mark the
Kibana task as failed\r\n\r\n#### ES task returns 404\r\nBoth cancelled
tasks and never started ones return 404 from ES side.\r\nThe only way to
tell the difference is to see if the Kibana task is “in\r\nprogress”
then this means it was cancelled from ES side (canelled via\r\nAPI by
the user directly for example) and we mark it as cancelled for\r\nthe
user.\r\n\r\n\r\n#### Other main flow tests:\r\n- Tested a small data
stream reindexing job\r\n- Tested a huge data stream reindexing job\r\n-
Tested pausing a job\r\n- Tested cancelling a job\r\n- Stopping ES
during reindexing and then restarting\r\n- Reindexing in a cluster with
2 Kibana
nodes","sha":"6925d129455ccb458e08cf33ce36c08e0d5313f5"}},"sourceBranch":"8.x","suggestedTargetBranches":["8.18"],"targetPullRequestStates":[{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Ahmad Bamieh 2025-01-31 01:46:54 +01:00 committed by GitHub
parent 396804f0f0
commit 160c0d4682
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 3124 additions and 7 deletions

View file

@ -0,0 +1,124 @@
/*
* 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 interface DataStreamsActionMetadata {
totalBackingIndices: number;
indicesRequiringUpgradeCount: number;
indicesRequiringUpgrade: string[];
ignoredIndicesRequiringUpgrade: string[];
ignoredIndicesRequiringUpgradeCount: number;
reindexRequired: boolean;
}
export interface DataStreamsAction {
type: 'dataStream';
metadata: DataStreamsActionMetadata;
}
export interface DataStreamMetadata {
dataStreamName: string;
documentationUrl: string;
lastIndexRequiringUpgradeCreationDate: number;
allIndices: string[];
allIndicesCount: number;
indicesRequiringUpgradeCount: number;
indicesRequiringUpgrade: string[];
indicesRequiringUpgradeDocsSize: number;
indicesRequiringUpgradeDocsCount: number;
}
export interface DataStreamReindexStatusResponse {
warnings?: DataStreamReindexWarning[];
reindexOp?: DataStreamReindexOperation;
hasRequiredPrivileges?: boolean;
}
export type DataStreamReindexWarningTypes = 'incompatibleDataStream';
export interface DataStreamReindexWarning {
warningType: DataStreamReindexWarningTypes;
meta?: {
[key: string]: string | string[];
};
}
export enum DataStreamReindexStatus {
notStarted,
inProgress,
completed,
failed,
cancelled,
fetchFailed,
}
export interface DataStreamProgressDetails {
startTimeMs: number;
successCount: number;
pendingCount: number;
inProgressCount: number;
errorsCount: number;
}
export interface DataStreamReindexStatusNotStarted {
status: DataStreamReindexStatus.notStarted;
}
export interface DataStreamReindexStatusInProgress {
status: DataStreamReindexStatus.inProgress;
reindexTaskPercComplete: number;
progressDetails: DataStreamProgressDetails;
}
export interface DataStreamReindexStatusCompleted {
status: DataStreamReindexStatus.completed;
reindexTaskPercComplete: number;
progressDetails: DataStreamProgressDetails;
}
export interface DataStreamReindexStatusFailed {
status: DataStreamReindexStatus.failed;
errorMessage: string;
}
export interface DataStreamReindexStatusCancelled {
status: DataStreamReindexStatus.cancelled;
}
export type DataStreamReindexOperation =
| DataStreamReindexStatusNotStarted
| DataStreamReindexStatusInProgress
| DataStreamReindexStatusCompleted
| DataStreamReindexStatusCancelled
| DataStreamReindexStatusFailed;
/**
* ES Requests Types (untyped in the ES Client)
*/
// Merged but not into our client version yet.
// https://github.com/elastic/elasticsearch-specification/blob/main/specification/migrate/get_reindex_status/MigrateGetReindexStatusResponse.ts
export interface DataStreamReindexTaskStatusResponse {
start_time?: number;
start_time_millis: number;
complete: boolean;
total_indices_in_data_stream: number;
total_indices_requiring_upgrade: number;
successes: number;
in_progress: Array<{
index: string;
total_doc_count: number;
reindexed_doc_count: number;
}>;
pending: number;
errors: Array<{
index: string;
message: string;
}>;
exception?: string;
}

View file

@ -8,6 +8,9 @@
import { HealthReportImpact } from '@elastic/elasticsearch/lib/api/types';
import type * as estypes from '@elastic/elasticsearch/lib/api/types';
import { SavedObject } from '@kbn/core/types';
import type { DataStreamsAction } from './data_stream_types';
export * from './data_stream_types';
export type DeprecationSource = 'Kibana' | 'Elasticsearch';
@ -229,6 +232,7 @@ export interface EnrichedDeprecationInfo
> {
type:
| keyof estypes.MigrationDeprecationsResponse
| 'data_streams'
| 'health_indicator'
| 'ilm_policies'
| 'templates';
@ -240,6 +244,7 @@ export interface EnrichedDeprecationInfo
| MlAction
| IndexSettingAction
| ClusterSettingAction
| DataStreamsAction
| HealthIndicatorAction;
resolveDuringUpgrade: boolean;
}

View file

@ -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 React, { createContext, useContext } from 'react';
import { ApiService } from '../../../../lib/api';
import { useReindexStatus, ReindexState } from './use_reindex_state';
export interface ReindexStateContext {
reindexState: ReindexState;
startReindex: () => Promise<void>;
loadDataStreamMetadata: () => Promise<void>;
cancelReindex: () => Promise<void>;
}
const DataStreamReindexContext = createContext<ReindexStateContext | undefined>(undefined);
export const useDataStreamReindexContext = () => {
const context = useContext(DataStreamReindexContext);
if (context === undefined) {
throw new Error(
'useDataStreamReindexContext must be used within a <DataStreamReindexStatusProvider />'
);
}
return context;
};
interface Props {
api: ApiService;
children: React.ReactNode;
dataStreamName: string;
}
export const DataStreamReindexStatusProvider: React.FunctionComponent<Props> = ({
api,
dataStreamName,
children,
}) => {
const { reindexState, startReindex, loadDataStreamMetadata, cancelReindex } = useReindexStatus({
dataStreamName,
api,
});
return (
<DataStreamReindexContext.Provider
value={{
reindexState,
startReindex,
cancelReindex,
loadDataStreamMetadata,
}}
>
{children}
</DataStreamReindexContext.Provider>
);
};

View file

@ -0,0 +1,279 @@
/*
* 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, useMemo, useState } from 'react';
import {
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutHeader,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import moment from 'moment';
import numeral from '@elastic/numeral';
import {
DataStreamReindexStatus,
EnrichedDeprecationInfo,
} from '../../../../../../../common/types';
import { ReindexStateContext } from '../context';
import { DeprecationBadge } from '../../../../shared';
import {
UIM_DATA_STREAM_REINDEX_START_CLICK,
UIM_DATA_STREAM_REINDEX_STOP_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 { DataStreamDetailsFlyoutStep } from './steps/details';
import { ChecklistFlyoutStep } from './steps/checklist';
import { ReindexingCompletedFlyoutStep } from './steps/completed';
interface Props extends ReindexStateContext {
deprecation: EnrichedDeprecationInfo;
closeFlyout: () => void;
}
const DATE_FORMAT = 'dddd, MMMM Do YYYY, h:mm:ss a';
const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b';
export const DataStreamReindexFlyout: React.FunctionComponent<Props> = ({
cancelReindex,
loadDataStreamMetadata,
reindexState,
startReindex,
closeFlyout,
deprecation,
}) => {
const { status, reindexWarnings, errorMessage, meta } = reindexState;
const { index } = deprecation;
const [flyoutStep, setFlyoutStep] = useState<FlyoutStep>('initializing');
const switchFlyoutStep = useCallback(() => {
switch (status) {
case DataStreamReindexStatus.notStarted: {
setFlyoutStep('notStarted');
return;
}
case DataStreamReindexStatus.failed:
case DataStreamReindexStatus.fetchFailed:
case DataStreamReindexStatus.cancelled:
case DataStreamReindexStatus.inProgress: {
setFlyoutStep('inProgress');
return;
}
case DataStreamReindexStatus.completed: {
setTimeout(() => {
// wait for 1.5 more seconds fur the UI to visually get to 100%
setFlyoutStep('completed');
}, 1500);
return;
}
}
}, [status]);
useMemo(async () => {
if (flyoutStep === 'initializing') {
await loadDataStreamMetadata();
switchFlyoutStep();
}
}, [loadDataStreamMetadata, switchFlyoutStep, flyoutStep]);
useMemo(() => switchFlyoutStep(), [switchFlyoutStep]);
const onStartReindex = useCallback(async () => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_REINDEX_START_CLICK);
await startReindex();
}, [startReindex]);
const onStopReindex = useCallback(async () => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_REINDEX_STOP_CLICK);
await cancelReindex();
}, [cancelReindex]);
const { docsSizeFormatted, indicesRequiringUpgradeDocsCount, lastIndexCreationDateFormatted } =
useMemo(() => {
if (!meta) {
return {
indicesRequiringUpgradeDocsCount: containerMessages.unknownMessage,
docsSizeFormatted: containerMessages.unknownMessage,
lastIndexCreationDateFormatted: containerMessages.unknownMessage,
};
}
return {
indicesRequiringUpgradeDocsCount:
typeof meta.indicesRequiringUpgradeDocsCount === 'number'
? `${meta.indicesRequiringUpgradeDocsCount}`
: 'Unknown',
docsSizeFormatted:
typeof meta.indicesRequiringUpgradeDocsSize === 'number'
? numeral(meta.indicesRequiringUpgradeDocsSize).format(FILE_SIZE_DISPLAY_FORMAT)
: 'Unknown',
lastIndexCreationDateFormatted:
typeof meta.lastIndexRequiringUpgradeCreationDate === 'number'
? `${moment(meta.lastIndexRequiringUpgradeCreationDate).format(DATE_FORMAT)}`
: 'Unknown',
};
}, [meta]);
const flyoutContents = useMemo(() => {
switch (flyoutStep) {
case 'initializing':
return <InitializingFlyoutStep errorMessage={errorMessage} />;
case 'notStarted': {
if (!meta) {
return (
<InitializingFlyoutStep
errorMessage={errorMessage || containerMessages.errorLoadingDataStreamInfo}
/>
);
}
return (
<DataStreamDetailsFlyoutStep
closeFlyout={closeFlyout}
lastIndexCreationDateFormatted={lastIndexCreationDateFormatted}
meta={meta}
startReindex={() => {
setFlyoutStep('confirm');
}}
reindexState={reindexState}
/>
);
}
case 'confirm': {
if (!meta) {
return (
<InitializingFlyoutStep
errorMessage={errorMessage || containerMessages.errorLoadingDataStreamInfo}
/>
);
}
return (
<ConfirmReindexingFlyoutStep
warnings={reindexWarnings ?? []}
meta={meta}
hideWarningsStep={() => {
setFlyoutStep('notStarted');
}}
continueReindex={() => {
onStartReindex();
}}
/>
);
}
case 'inProgress': {
if (!meta) {
return (
<InitializingFlyoutStep
errorMessage={errorMessage || containerMessages.errorLoadingDataStreamInfo}
/>
);
}
return (
<ChecklistFlyoutStep
closeFlyout={closeFlyout}
startReindex={() => {
setFlyoutStep('confirm');
}}
reindexState={reindexState}
cancelReindex={onStopReindex}
/>
);
}
case 'completed': {
if (!meta) {
return (
<InitializingFlyoutStep
errorMessage={errorMessage || containerMessages.errorLoadingDataStreamInfo}
/>
);
}
return <ReindexingCompletedFlyoutStep meta={meta} />;
}
}
}, [
flyoutStep,
reindexState,
closeFlyout,
onStartReindex,
onStopReindex,
lastIndexCreationDateFormatted,
reindexWarnings,
meta,
errorMessage,
]);
return (
<>
{flyoutStep !== 'initializing' && (
<EuiFlyoutHeader hasBorder>
<DeprecationBadge
isCritical={deprecation.isCritical}
isResolved={status === DataStreamReindexStatus.completed}
/>
<EuiSpacer size="s" />
<EuiTitle size="s" data-test-subj="flyoutTitle">
<h2 id="reindexDetailsFlyoutTitle">{index}</h2>
</EuiTitle>
{meta && (
<>
<EuiSpacer size="m" />
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiDescriptionList
textStyle="reverse"
listItems={[
{
title: 'Reindexing required for indices created on or before',
description: lastIndexCreationDateFormatted,
},
]}
/>
</EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem>
<EuiDescriptionList
textStyle="reverse"
listItems={[
{
title: 'Size',
description: docsSizeFormatted,
},
]}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiDescriptionList
textStyle="reverse"
listItems={[
{
title: 'Document Count',
description: indicesRequiringUpgradeDocsCount,
},
]}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</>
)}
</EuiFlyoutHeader>
)}
{flyoutContents}
</>
);
};

View file

@ -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 { DataStreamReindexFlyout } from './container';

View file

@ -0,0 +1,57 @@
/*
* 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 { DataStreamReindexStatus } from '../../../../../../../common/types';
export const getPrimaryButtonLabel = (status?: DataStreamReindexStatus) => {
switch (status) {
case DataStreamReindexStatus.fetchFailed:
case DataStreamReindexStatus.failed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.reindexButton.tryAgainLabel"
defaultMessage="Try again"
/>
);
case DataStreamReindexStatus.inProgress:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.reindexButton.reindexingLabel"
defaultMessage="Reindexing…"
/>
);
case DataStreamReindexStatus.cancelled:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.reindexButton.restartLabel"
defaultMessage="Restart reindexing"
/>
);
default:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.reindexButton.runReindexLabel"
defaultMessage="Start reindexing"
/>
);
}
};
export const containerMessages = {
unknownMessage: i18n.translate('xpack.upgradeAssistant.dataStream.flyout.unknownMessage', {
defaultMessage: 'Unknown',
}),
errorLoadingDataStreamInfo: i18n.translate(
'xpack.upgradeAssistant.dataStream.flyout.errorLoadingDataStreamInfo',
{
defaultMessage: 'Error loading data stream info',
}
),
};

View file

@ -0,0 +1,203 @@
/*
* 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,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataStreamReindexStatus } from '../../../../../../../../../common/types';
import { LoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex_state';
import { ReindexProgress } from './progress';
import { useAppContext } from '../../../../../../../app_context';
import { getPrimaryButtonLabel } from '../../messages';
/**
* Displays a flyout that shows the current reindexing status for a given index.
*/
export const ChecklistFlyoutStep: React.FunctionComponent<{
closeFlyout: () => void;
reindexState: ReindexState;
startReindex: () => void;
cancelReindex: () => void;
}> = ({ closeFlyout, reindexState, startReindex, cancelReindex }) => {
const {
services: { api },
} = useAppContext();
const { loadingState, status, hasRequiredPrivileges } = reindexState;
const loading =
loadingState === LoadingState.Loading || status === DataStreamReindexStatus.inProgress;
const isCompleted = status === DataStreamReindexStatus.completed;
const hasFetchFailed = status === DataStreamReindexStatus.fetchFailed;
const hasReindexingFailed = status === DataStreamReindexStatus.failed;
const { data: nodes } = api.useLoadNodeDiskSpace();
const showMainButton = !hasFetchFailed && !isCompleted && hasRequiredPrivileges;
const shouldShowCancelButton = showMainButton && status === DataStreamReindexStatus.inProgress;
return (
<Fragment>
<EuiFlyoutBody>
{hasRequiredPrivileges === false && (
<Fragment>
<EuiSpacer />
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.insufficientPrivilegeCallout.calloutTitle"
defaultMessage="You do not have sufficient privileges to reindex this index"
/>
}
color="danger"
iconType="warning"
/>
</Fragment>
)}
{nodes && nodes.length > 0 && (
<>
<EuiCallOut
color="warning"
iconType="warning"
data-test-subj="lowDiskSpaceCallout"
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.lowDiskSpaceCalloutTitle"
defaultMessage="Nodes with low disk space"
/>
}
>
<>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.lowDiskSpaceCalloutDescription"
defaultMessage="Disk usage has exceeded the low watermark, which may prevent reindexing. The following nodes are impacted:"
/>
<EuiSpacer size="s" />
<ul>
{nodes.map(({ nodeName, available, nodeId }) => (
<li key={nodeId} data-test-subj="impactedNodeListItem">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.lowDiskSpaceUsedText"
defaultMessage="{nodeName} ({available} available)"
values={{
nodeName,
available,
}}
/>
</li>
))}
</ul>
</>
</EuiCallOut>
<EuiSpacer />
</>
)}
{(hasFetchFailed || hasReindexingFailed) && (
<>
<EuiCallOut
color="danger"
iconType="warning"
data-test-subj={hasFetchFailed ? 'fetchFailedCallout' : 'reindexingFailedCallout'}
title={
hasFetchFailed ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.fetchFailedCalloutTitle"
defaultMessage="Reindex status not available"
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingFailedCalloutTitle"
defaultMessage="Reindexing error"
/>
)
}
>
{reindexState.errorMessage}
</EuiCallOut>
<EuiSpacer />
</>
)}
<EuiText>
<p>
<FormattedMessage
defaultMessage="Reindexing is performed in the background. You can return to the Upgrade Assistant to view progress."
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail"
/>
</p>
</EuiText>
<EuiSpacer />
<ReindexProgress reindexState={reindexState} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
{shouldShowCancelButton && (
<EuiFlexItem grow={false}>
<EuiButton
color={'accent'}
iconType={'pause'}
onClick={cancelReindex}
disabled={!hasRequiredPrivileges}
data-test-subj="cancelDataStreamReindexingButton"
>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.cancelReindexButtonLabel"
defaultMessage="Cancel reindexing"
/>
</EuiButton>
</EuiFlexItem>
)}
{showMainButton && (
<EuiFlexItem grow={false}>
<EuiButton
fill
color={status === DataStreamReindexStatus.inProgress ? 'primary' : 'warning'}
iconType={status === DataStreamReindexStatus.inProgress ? undefined : 'refresh'}
onClick={startReindex}
isLoading={loading}
disabled={loading || !hasRequiredPrivileges}
data-test-subj="startDataStreamReindexingButton"
>
{getPrimaryButtonLabel(status)}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
);
};

View file

@ -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 { ChecklistFlyoutStep } from './checklist_step';

View file

@ -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 from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
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 { getDataStreamReindexProgress } from '../../../../../../../lib/utils';
import { ReindexingDocumentsStepTitle } from './progress_title';
import { CancelLoadingState } from '../../../../../../types';
interface Props {
reindexState: ReindexState;
}
/**
* 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> = (props) => {
const { status, reindexTaskPercComplete, cancelLoadingState, taskStatus } = props.reindexState;
// The reindexing step is special because it generally lasts longer and can be cancelled mid-flight
const reindexingDocsStep = {
title: (
<EuiFlexGroup component="span">
<EuiFlexItem grow={false}>
<ReindexingDocumentsStepTitle {...props} />
</EuiFlexItem>
</EuiFlexGroup>
),
} as StepProgressStep;
const inProgress =
status === DataStreamReindexStatus.inProgress || status === DataStreamReindexStatus.completed;
let euiProgressColor = 'subdued';
if (cancelLoadingState === CancelLoadingState.Error) {
reindexingDocsStep.status = 'failed';
euiProgressColor = 'danger';
} else if (
cancelLoadingState === CancelLoadingState.Loading ||
cancelLoadingState === CancelLoadingState.Requested
) {
reindexingDocsStep.status = 'inProgress';
euiProgressColor = 'subdued';
} else if (status === DataStreamReindexStatus.failed) {
reindexingDocsStep.status = 'failed';
euiProgressColor = 'danger';
} else if (
status === DataStreamReindexStatus.cancelled ||
cancelLoadingState === CancelLoadingState.Success
) {
reindexingDocsStep.status = 'cancelled';
} else if (status === undefined) {
reindexingDocsStep.status = 'incomplete';
euiProgressColor = 'subdued';
} else if (status === DataStreamReindexStatus.inProgress) {
reindexingDocsStep.status = 'inProgress';
euiProgressColor = 'primary';
} else if (status === DataStreamReindexStatus.completed) {
reindexingDocsStep.status = 'complete';
euiProgressColor = 'success';
} else {
// not started // undefined
reindexingDocsStep.status = 'incomplete';
euiProgressColor = 'subdued';
}
const progressPercentage = inProgress
? getDataStreamReindexProgress(status, reindexTaskPercComplete)
: undefined;
const showProgressValueText = inProgress;
const progressMaxValue = inProgress ? 100 : undefined;
return (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
<EuiTitle size="xs" data-test-subj="reindexChecklistTitle">
<h3>
{status === DataStreamReindexStatus.inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingInProgressTitle"
defaultMessage="Reindexing in progress…"
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklistTitle"
defaultMessage="Reindex data stream"
/>
)}
</h3>
</EuiTitle>
</EuiFlexItem>
{inProgress && (
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiProgress
label={
taskStatus ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingInProgressTitle"
defaultMessage="Started {startTimeFromNow}"
values={{
startTimeFromNow: (
<FormattedRelativeTime
value={(taskStatus.startTimeMs - +moment()) / 1000}
updateIntervalInSeconds={1}
/>
),
}}
/>
) : undefined
}
valueText={showProgressValueText}
value={progressPercentage}
max={progressMaxValue}
color={euiProgressColor}
size="m"
/>
</EuiFlexItem>
)}
<EuiFlexItem>
<StepProgress steps={[reindexingDocsStep]} />
</EuiFlexItem>
{inProgress && (
<EuiFlexItem>
{!taskStatus && (
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.fetchingStatus"
defaultMessage="Fetching Status…"
/>
</p>
)}
{taskStatus && (
<EuiFlexGroup direction="column" gutterSize="xs" style={{ padding: '0 28px' }}>
{taskStatus.errorsCount > 0 && (
<EuiFlexItem>
<EuiText size="s" color="danger">
<p>
{i18n.translate(
'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.failedTitle',
{
defaultMessage:
'{count, plural, =1 {# Index} other {# Indices}} failed to reindex.',
values: { count: taskStatus.errorsCount },
}
)}
</p>
</EuiText>
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiText size="s" color="success">
<p>
{i18n.translate(
'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.completeTitle',
{
defaultMessage:
'{count, plural, =1 {# Index} other {# Indices}} successfully reindexed.',
values: { count: taskStatus.successCount },
}
)}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" color="primary">
<p>
{i18n.translate(
'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.inProgressTitle',
{
defaultMessage:
'{count, plural, =1 {# Index} other {# Indices}} currently reindexing.',
values: { count: taskStatus.inProgressCount },
}
)}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s">
<p>
{i18n.translate(
'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.pendingTitle',
{
defaultMessage:
'{count, plural, =1 {# Index} other {# Indices}} waiting to start.',
values: { count: taskStatus.pendingCount },
}
)}
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,93 @@
/*
* 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 { CancelLoadingState } from '../../../../../../types';
import { DataStreamReindexStatus } from '../../../../../../../../../common/types';
import type { ReindexState } from '../../../use_reindex_state';
export const ReindexingDocumentsStepTitle: React.FunctionComponent<{
reindexState: ReindexState;
}> = ({ reindexState: { status, cancelLoadingState } }) => {
switch (cancelLoadingState) {
case CancelLoadingState.Requested:
case CancelLoadingState.Loading: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancellingLabel"
defaultMessage="Cancelling…"
/>
);
}
case CancelLoadingState.Success: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancelledLabel"
defaultMessage="Cancelled"
/>
);
}
case CancelLoadingState.Error: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.errorLabel"
defaultMessage="Failed to cancel reindexing"
/>
);
}
}
switch (status) {
case DataStreamReindexStatus.inProgress: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.reindexingDocumentsStepTitle"
defaultMessage="Reindexing data stream"
/>
);
}
case DataStreamReindexStatus.failed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.failed.reindexingDocumentsStepTitle"
defaultMessage="Reindexing failed"
/>
);
case DataStreamReindexStatus.fetchFailed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.fetchFailed.reindexingDocumentsStepTitle"
defaultMessage="Fetching status failed"
/>
);
case DataStreamReindexStatus.cancelled:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.cancelled.reindexingDocumentsStepTitle"
defaultMessage="Reindexing cancelled"
/>
);
case DataStreamReindexStatus.completed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.completed.reindexingDocumentsStepTitle"
defaultMessage="Reindexing completed"
/>
);
case DataStreamReindexStatus.notStarted:
default: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.reindexingDocumentsStepTitle"
defaultMessage="Reindex data stream"
/>
);
}
}
};

View file

@ -0,0 +1,41 @@
/*
* 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 { EuiFlyoutBody, EuiSpacer, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DataStreamMetadata } from '../../../../../../../../../common/types';
interface Props {
meta: DataStreamMetadata;
}
export const ReindexingCompletedFlyoutStep: React.FunctionComponent<Props> = ({ meta }: Props) => {
return (
<>
<EuiFlyoutBody>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.acceptChangesTitle"
defaultMessage="Data Stream Reindexing Complete"
/>
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.acceptChangesTitle"
defaultMessage="Success! {count, plural, =1 {# backing index} other {# backing indices}} successfully reindexed."
values={{ count: meta.indicesRequiringUpgradeCount }}
/>
</p>
</EuiFlyoutBody>
</>
);
};

View file

@ -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 { ReindexingCompletedFlyoutStep } from './completed_step';

View file

@ -0,0 +1,157 @@
/*
* 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,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
DataStreamReindexWarning,
DataStreamReindexWarningTypes,
DataStreamMetadata,
} from '../../../../../../../../../common/types';
import { useAppContext } from '../../../../../../../app_context';
import {
IncompatibleDataInDataStreamWarningCheckbox,
WarningCheckboxProps,
} from './warning_step_checkbox';
interface CheckedIds {
[id: string]: boolean;
}
const warningToComponentMap: Record<
DataStreamReindexWarningTypes,
React.FunctionComponent<WarningCheckboxProps>
> = {
incompatibleDataStream: IncompatibleDataInDataStreamWarningCheckbox,
};
export const idForWarning = (id: number) => `reindexWarning-${id}`;
interface WarningsConfirmationFlyoutProps {
hideWarningsStep: () => void;
continueReindex: () => void;
warnings: DataStreamReindexWarning[];
meta: DataStreamMetadata;
}
/**
* Displays warning text about destructive changes required to reindex this index. The user
* must acknowledge each change before being allowed to proceed.
*/
export const ConfirmReindexingFlyoutStep: React.FunctionComponent<
WarningsConfirmationFlyoutProps
> = ({ warnings, hideWarningsStep, continueReindex, meta }) => {
const {
services: {
core: { docLinks },
},
} = useAppContext();
const { links } = docLinks;
const [checkedIds, setCheckedIds] = useState<CheckedIds>(
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<HTMLInputElement>) => {
const optionId = e.target.id;
setCheckedIds((prev) => ({
...prev,
...{
[optionId]: !checkedIds[optionId],
},
}));
};
return (
<>
<EuiFlyoutBody>
{warnings.length > 0 && (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle"
defaultMessage="This operation requires destructive changes that cannot be reversed"
/>
}
color="warning"
iconType="warning"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail"
defaultMessage="Ensure data has been backed up before continuing. To proceed with reindexing this data, confirm below."
/>
</p>
</EuiCallOut>
<EuiSpacer />
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.dataStreamReindexing.flyout.warningsStep.acceptChangesTitle"
defaultMessage="{count, plural, =1 {# backing index} other {# backing indices}}, including current write index, will be re-indexed. Current write index will be rolled over first."
values={{ count: meta.indicesRequiringUpgradeCount }}
/>
</p>
<EuiSpacer size="m" />
{warnings.map((warning, index) => {
const WarningCheckbox = warningToComponentMap[warning.warningType];
return (
<WarningCheckbox
key={idForWarning(index)}
isChecked={checkedIds[idForWarning(index)]}
onChange={onChange}
docLinks={links}
id={idForWarning(index)}
// @ts-ignore
meta={{ ...meta, ...warning.meta }}
/>
);
})}
</>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="arrowLeft" onClick={hideWarningsStep} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.backButtonLabel"
defaultMessage="Back"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill color="primary" onClick={continueReindex} disabled={blockAdvance}>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.startReindexingButtonLabel"
defaultMessage="Start reindexing"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
);
};

View file

@ -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 { ConfirmReindexingFlyoutStep } from './confirm_step';

View file

@ -0,0 +1,103 @@
/*
* 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 {
EuiCheckbox,
EuiLink,
EuiSpacer,
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
} 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<{
isChecked: boolean;
warningId: string;
label: React.ReactNode;
description: React.ReactNode;
documentationUrl?: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}> = ({ isChecked, warningId, label, onChange, description, documentationUrl }) => (
<>
<EuiText>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiCheckbox
id={warningId}
label={<strong>{label}</strong>}
checked={isChecked}
onChange={onChange}
/>
</EuiFlexItem>
{documentationUrl !== undefined && (
<EuiFlexItem grow={false}>
<EuiLink href={documentationUrl} target="_blank" external={false}>
<EuiIconTip
content={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.documentationLinkLabel"
defaultMessage="Documentation"
/>
}
position="right"
type="help"
/>
</EuiLink>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer size="xs" />
{description}
</EuiText>
<EuiSpacer />
</>
);
export interface WarningCheckboxProps {
isChecked: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
docLinks: DocLinksStart['links'];
id: string;
}
export const IncompatibleDataInDataStreamWarningCheckbox: React.FunctionComponent<
WarningCheckboxProps
> = ({ isChecked, onChange, id }) => {
return (
<WarningCheckbox
isChecked={isChecked}
onChange={onChange}
warningId={id}
label={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.incompatibleDataWarningTitle"
defaultMessage="Reindex all incompatible data for this data stream"
/>
}
description={null}
/>
);
};

View file

@ -0,0 +1,253 @@
/*
* 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,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiLink,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
DataStreamMetadata,
DataStreamReindexStatus,
} from '../../../../../../../../../common/types';
import { LoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex_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.
*/
export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
closeFlyout: () => void;
reindexState: ReindexState;
startReindex: () => void;
lastIndexCreationDateFormatted: string;
meta: DataStreamMetadata;
}> = ({ closeFlyout, reindexState, startReindex, lastIndexCreationDateFormatted, meta }) => {
const {
services: {
api,
core: { http },
},
} = useAppContext();
const { loadingState, status, hasRequiredPrivileges } = reindexState;
const loading =
loadingState === LoadingState.Loading || status === DataStreamReindexStatus.inProgress;
const isCompleted = status === DataStreamReindexStatus.completed;
const hasFetchFailed = status === DataStreamReindexStatus.fetchFailed;
const hasReindexingFailed = status === DataStreamReindexStatus.failed;
const { data: nodes } = api.useLoadNodeDiskSpace();
return (
<Fragment>
<EuiFlyoutBody>
<DurationClarificationCallOut
formattedDate={lastIndexCreationDateFormatted}
learnMoreUrl={meta.documentationUrl}
/>
<EuiSpacer size="m" />
{hasRequiredPrivileges === false && (
<Fragment>
<EuiSpacer />
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.insufficientPrivilegeCallout.calloutTitle"
defaultMessage="You do not have sufficient privileges to reindex this data stream."
/>
}
color="danger"
iconType="warning"
/>
</Fragment>
)}
{nodes && nodes.length > 0 && (
<>
<EuiCallOut
color="warning"
iconType="warning"
data-test-subj="lowDiskSpaceCallout"
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.lowDiskSpaceCalloutTitle"
defaultMessage="Nodes with low disk space"
/>
}
>
<>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.lowDiskSpaceCalloutDescription"
defaultMessage="Disk usage has exceeded the low watermark, which may prevent reindexing. The following nodes are impacted:"
/>
<EuiSpacer size="s" />
<ul>
{nodes.map(({ nodeName, available, nodeId }) => (
<li key={nodeId} data-test-subj="impactedNodeListItem">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.lowDiskSpaceUsedText"
defaultMessage="{nodeName} ({available} available)"
values={{
nodeName,
available,
}}
/>
</li>
))}
</ul>
</>
</EuiCallOut>
<EuiSpacer />
</>
)}
{(hasFetchFailed || hasReindexingFailed) && (
<>
<EuiCallOut
color="danger"
iconType="warning"
data-test-subj={hasFetchFailed ? 'fetchFailedCallout' : 'reindexingFailedCallout'}
title={
hasFetchFailed ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.fetchFailedCalloutTitle"
defaultMessage="Data stream reindex status not available"
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexingFailedCalloutTitle"
defaultMessage="Data stream reindexing error"
/>
)
}
>
{reindexState.errorMessage}
</EuiCallOut>
<EuiSpacer />
</>
)}
<EuiText>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.notCompatibleIndicesText"
defaultMessage="You have {backingIndicesCount} backing indices on this data stream that were created in ES 7.x and will not be compatible with next version."
values={{
backingIndicesCount: meta.indicesRequiringUpgradeCount,
}}
/>
</p>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.requiredUpgradeText"
defaultMessage="{allBackingIndices} total backing indices, and {backingIndicesRequireingUpgrade} requires upgrade."
values={{
allBackingIndices: meta.allIndicesCount,
backingIndicesRequireingUpgrade: meta.indicesRequiringUpgradeCount,
}}
/>
</p>
<ul>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.readOnlyText"
tagName="li"
defaultMessage="If you do not need to update historical data, mark as read-only. You can reindex post-upgrade if updates are needed."
/>
<li>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexOptionListTitle"
defaultMessage="Reindex"
/>
<ul>
<FormattedMessage
tagName="li"
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexOption.rolledOverIndex"
defaultMessage="The current write index will be rolled over and reindexed."
/>
<FormattedMessage
tagName="li"
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexOption.additionalIndices"
defaultMessage="Additional backing indices will be reindexed and remain editable."
/>
</ul>
</li>
</ul>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexDescription"
defaultMessage="If you no longer need this data, you can also proceed by deleting these indices. {indexManagementLinkHtml}"
values={{
indexManagementLinkHtml: (
<EuiLink
href={`${http.basePath.prepend(
'/app/management/data/index_management/indices'
)}`}
>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.indexMgmtLink"
defaultMessage="Go to index management"
/>
</EuiLink>
),
}}
/>
</p>
</EuiText>
<EuiSpacer />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
{!hasFetchFailed && !isCompleted && hasRequiredPrivileges && (
<EuiFlexItem grow={false}>
<EuiButton
color={status === DataStreamReindexStatus.cancelled ? 'warning' : 'primary'}
iconType={status === DataStreamReindexStatus.cancelled ? 'play' : undefined}
onClick={startReindex}
isLoading={loading}
disabled={loading || !hasRequiredPrivileges}
data-test-subj="startReindexingButton"
>
{getPrimaryButtonLabel(status)}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
);
};

View file

@ -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 { DataStreamDetailsFlyoutStep } from './details_step';

View file

@ -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 { EuiCallOut, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
interface Props {
formattedDate: string;
learnMoreUrl: string;
}
export const DurationClarificationCallOut: React.FunctionComponent<Props> = ({
formattedDate,
learnMoreUrl,
}) => {
return (
<EuiCallOut color="primary">
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.indicesNeedReindexing"
defaultMessage="Indices created on or before {formattedDate} need to be reindexed to a compatible format or marked as read-only."
values={{ formattedDate }}
/>
<br />
<br />
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.suggestReadOnly"
defaultMessage="Depending on size and resources, reindexing may take extended time and your data will be in a read-only state until the job has completed. {learnMoreHtml}"
values={{
learnMoreHtml: (
<EuiLink href={learnMoreUrl} target="_blank">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.learnMoreLink"
defaultMessage="Learn more"
/>
</EuiLink>
),
}}
/>
</p>
</EuiCallOut>
);
};

View file

@ -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 { InitializingFlyoutStep } from './initializing_step';

View file

@ -0,0 +1,67 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiIcon,
EuiLoadingSpinner,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
interface InitializingFlyoutStepProps {
errorMessage?: string | null;
}
export const InitializingFlyoutStep: React.FunctionComponent<InitializingFlyoutStepProps> = ({
errorMessage,
}) => {
const hasInitializingError = !!errorMessage;
return (
<>
<EuiFlyoutBody>
<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">
<EuiFlexItem>
{hasInitializingError ? (
<EuiIcon type="alert" size="xl" color="danger" />
) : (
<EuiLoadingSpinner size="xl" />
)}
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="s">
{hasInitializingError ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.initializingStep.errorLoadingDataStreamInfo"
defaultMessage="Error loading data stream info"
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.initializingStep.loadingDataStreamInfo"
defaultMessage="Loading Data stream info"
/>
)}
</EuiTitle>
</EuiFlexItem>
{hasInitializingError && (
<EuiFlexItem>
<EuiText>{errorMessage}</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutBody>
</>
);
};

View file

@ -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 FlyoutStep = 'initializing' | 'notStarted' | 'confirm' | 'inProgress' | 'completed';

View file

@ -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 { DataStreamTableRow } from './table_row';

View file

@ -0,0 +1,157 @@
/*
* 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 { i18n } from '@kbn/i18n';
import {
EuiIcon,
EuiLoadingSpinner,
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
} from '@elastic/eui';
import { DataStreamReindexStatus } from '../../../../../../common/types';
import { getDataStreamReindexProgressLabel } from '../../../../lib/utils';
import { LoadingState } from '../../../types';
import { useDataStreamReindexContext } from './context';
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.',
}
),
};
export const DataStreamReindexResolutionCell: React.FunctionComponent = () => {
const { reindexState } = useDataStreamReindexContext();
if (reindexState.loadingState === LoadingState.Loading) {
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexLoadingStatusText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
switch (reindexState.status) {
case DataStreamReindexStatus.inProgress:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
{i18nTexts.reindexInProgressText}{' '}
{getDataStreamReindexProgressLabel(
reindexState.status,
reindexState.reindexTaskPercComplete
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
case DataStreamReindexStatus.completed:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="check" color="success" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexCompleteText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
case DataStreamReindexStatus.failed:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="warning" color="danger" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexFailedText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
case DataStreamReindexStatus.fetchFailed:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="warning" color="danger" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexFetchFailedText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
default:
return (
<EuiToolTip position="top" content={i18nTexts.resolutionTooltipLabel}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="indexSettings" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.resolutionText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
);
}
};

View file

@ -0,0 +1,105 @@
/*
* 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, useEffect, useCallback } from 'react';
import { EuiTableRowCell } from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import { EnrichedDeprecationInfo } from '../../../../../../common/types';
import { GlobalFlyout } from '../../../../../shared_imports';
import { useAppContext } from '../../../../app_context';
import {
uiMetricService,
UIM_DATA_STREAM_REINDEX_CLOSE_FLYOUT_CLICK,
UIM_DATA_STREAM_REINDEX_OPEN_FLYOUT_CLICK,
} from '../../../../lib/ui_metric';
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';
const { useGlobalFlyout } = GlobalFlyout;
interface TableRowProps {
deprecation: EnrichedDeprecationInfo;
rowFieldNames: DeprecationTableColumns[];
}
const DataStreamTableRowCells: React.FunctionComponent<TableRowProps> = ({
rowFieldNames,
deprecation,
}) => {
const [showFlyout, setShowFlyout] = useState(false);
const dataStreamContext = useDataStreamReindexContext();
const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } =
useGlobalFlyout();
const closeFlyout = useCallback(async () => {
removeContentFromGlobalFlyout('dataStreamReindexFlyout');
setShowFlyout(false);
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_REINDEX_CLOSE_FLYOUT_CLICK);
}, [removeContentFromGlobalFlyout]);
useEffect(() => {
if (showFlyout) {
addContentToGlobalFlyout({
id: 'dataStreamReindexFlyout',
Component: DataStreamReindexFlyout,
props: {
...dataStreamContext,
deprecation,
closeFlyout,
},
flyoutProps: {
onClose: closeFlyout,
className: 'eui-textBreakWord',
'data-test-subj': 'reindexDetails',
'aria-labelledby': 'reindexDetailsFlyoutTitle',
},
});
}
}, [addContentToGlobalFlyout, deprecation, dataStreamContext, showFlyout, closeFlyout]);
useEffect(() => {
if (showFlyout) {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_REINDEX_OPEN_FLYOUT_CLICK);
}
}, [showFlyout]);
return (
<>
{rowFieldNames.map((field: DeprecationTableColumns) => {
return (
<EuiTableRowCell
key={field}
truncateText={false}
data-test-subj={`dataStreamReindexTableCell-${field}`}
>
<EsDeprecationsTableCells
fieldName={field}
openFlyout={() => setShowFlyout(true)}
deprecation={deprecation}
resolutionTableCell={<DataStreamReindexResolutionCell />}
/>
</EuiTableRowCell>
);
})}
</>
);
};
export const DataStreamTableRow: React.FunctionComponent<TableRowProps> = (props) => {
const {
services: { api },
} = useAppContext();
return (
<DataStreamReindexStatusProvider dataStreamName={props.deprecation.index!} api={api}>
<DataStreamTableRowCells {...props} />
</DataStreamReindexStatusProvider>
);
};

View file

@ -0,0 +1,284 @@
/*
* 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<ReindexState>({
loadingState: LoadingState.Loading,
errorMessage: null,
reindexTaskPercComplete: null,
taskStatus: undefined,
meta: null,
});
const pollIntervalIdRef = useRef<ReturnType<typeof setTimeout> | 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,
};
};

View file

@ -9,5 +9,6 @@ export { MlSnapshotsTableRow } from './ml_snapshots';
export { IndexSettingsTableRow } from './index_settings';
export { DefaultTableRow } from './default';
export { ReindexTableRow } from './reindex';
export { DataStreamTableRow } from './data_streams';
export { ClusterSettingsTableRow } from './cluster_settings';
export { HealthIndicatorTableRow } from './health_indicator';

View file

@ -34,6 +34,7 @@ import {
ReindexTableRow,
ClusterSettingsTableRow,
HealthIndicatorTableRow,
DataStreamTableRow,
} from './deprecation_types';
import { DeprecationTableColumns } from '../types';
import { DEPRECATION_TYPE_MAP, PAGINATION_CONFIG } from '../constants';
@ -130,6 +131,9 @@ const renderTableRowCells = (
case 'healthIndicator':
return <HealthIndicatorTableRow deprecation={deprecation} rowFieldNames={cellTypes} />;
case 'dataStream':
return <DataStreamTableRow deprecation={deprecation} rowFieldNames={cellTypes} />;
default:
return <DefaultTableRow deprecation={deprecation} rowFieldNames={cellTypes} />;
}

View file

@ -14,6 +14,8 @@ import {
ResponseError,
SystemIndicesMigrationStatus,
ReindexStatusResponse,
DataStreamReindexStatusResponse,
DataStreamMetadata,
} from '../../../common/types';
import {
API_BASE_PATH,
@ -209,6 +211,41 @@ export class ApiService {
});
}
public async getDataStreamReindexStatus(dataStreamName: string) {
return await this.sendRequest<DataStreamReindexStatusResponse>({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}`,
method: 'get',
});
}
public async getDataStreamMetadata(dataStreamName: string) {
return await this.sendRequest<DataStreamMetadata>({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}/metadata`,
method: 'get',
});
}
public async startDataStreamReindexTask(dataStreamName: string) {
return await this.sendRequest({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}`,
method: 'post',
});
}
public async cancelDataStreamReindexTask(dataStreamName: string) {
return await this.sendRequest({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}/cancel`,
method: 'post',
});
}
public async pauseDataStreamReindexTask(dataStreamName: string) {
return await this.sendRequest({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}/pause`,
method: 'post',
});
}
public async getReindexStatus(indexName: string) {
return await this.sendRequest<ReindexStatusResponse>({
path: `${API_BASE_PATH}/reindex/${indexName}`,

View file

@ -13,10 +13,18 @@ export const UIM_ES_DEPRECATIONS_PAGE_LOAD = 'es_deprecations_page_load';
export const UIM_KIBANA_DEPRECATIONS_PAGE_LOAD = 'kibana_deprecations_page_load';
export const UIM_OVERVIEW_PAGE_LOAD = 'overview_page_load';
export const UIM_ES_DEPRECATION_LOGS_PAGE_LOAD = 'es_deprecation_logs_page_load';
// Reindexing
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';
// 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_BACKUP_DATA_CLOUD_CLICK = 'backup_data_cloud_click';
export const UIM_BACKUP_DATA_ON_PREM_CLICK = 'backup_data_on_prem_click';
export const UIM_RESET_LOGS_COUNTER_CLICK = 'reset_logs_counter_click';

View file

@ -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 } from '../../../common/types';
import { ReindexStep, DataStreamReindexStatus } from '../../../common/types';
export const validateRegExpString = (s: string) =>
pipe(
@ -101,3 +101,33 @@ export const getReindexProgressLabel = (
}
return `${percentsComplete}%`;
};
export const getDataStreamReindexProgress = (
status: DataStreamReindexStatus,
reindexTaskPercComplete: number | null
): number => {
switch (status) {
case DataStreamReindexStatus.notStarted:
return 0;
case DataStreamReindexStatus.fetchFailed:
case DataStreamReindexStatus.failed:
case DataStreamReindexStatus.cancelled:
case DataStreamReindexStatus.inProgress: {
return reindexTaskPercComplete !== null ? Math.round(reindexTaskPercComplete * 100) : 0;
}
case DataStreamReindexStatus.completed: {
return 100;
}
}
return 0;
};
export const getDataStreamReindexProgressLabel = (
status: DataStreamReindexStatus,
reindexTaskPercComplete: number | null
): string => {
const percentsComplete = getDataStreamReindexProgress(status, reindexTaskPercComplete);
return `${percentsComplete}%`;
};

View file

@ -0,0 +1,325 @@
/*
* 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, Logger } from '@kbn/core/server';
import { firstValueFrom } from 'rxjs';
import { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
import { TransportResult } from '@elastic/elasticsearch';
import _ from 'lodash';
import {
DataStreamReindexStatus,
DataStreamReindexOperation,
DataStreamMetadata,
DataStreamReindexWarning,
DataStreamReindexTaskStatusResponse,
DataStreamReindexStatusCancelled,
} from '../../../common/types';
import { error } from './error';
interface DataStreamReindexService {
/**
* Checks whether or not the user has proper privileges required to reindex this index.
* @param dataStreamName
*/
hasRequiredPrivileges: (dataStreamName: string) => Promise<boolean>;
/**
* Checks an index's settings and mappings to flag potential issues during reindex.
* Resolves to null if index does not exist.
* @param dataStreamName
*/
detectReindexWarnings: (
dataStreamName: string
) => Promise<DataStreamReindexWarning[] | undefined>;
/**
* Creates a new reindex operation for a given index.
* @param dataStreamName
*/
createReindexOperation: (dataStreamName: string) => Promise<boolean>;
/**
* Polls Elasticsearch's Data stream status API to retrieve the status of the reindex operation.
* @param dataStreamName
*/
fetchReindexStatus: (dataStreamName: string) => Promise<DataStreamReindexOperation>;
/**
* Cancels an in-progress reindex operation for a given index.
* @param dataStreamName
*/
cancelReindexing: (dataStreamName: string) => Promise<DataStreamReindexStatusCancelled>;
/**
* Retrieves metadata about the data stream.
* @param dataStreamName
*/
getDataStreamMetadata: (dataStreamName: string) => Promise<DataStreamMetadata | null>;
}
export interface DataStreamReindexServiceFactoryParams {
esClient: ElasticsearchClient;
log: Logger;
licensing: LicensingPluginSetup;
}
export const dataStreamReindexServiceFactory = ({
esClient,
licensing,
}: DataStreamReindexServiceFactoryParams): DataStreamReindexService => {
return {
hasRequiredPrivileges: async (dataStreamName: string): Promise<boolean> => {
/**
* To avoid a circular dependency on Security we use a work around
* here to detect whether Security is available and enabled
* (i.e., via the licensing plugin). This enables Security to use
* functionality exposed through Upgrade Assistant.
*/
const license = await firstValueFrom(licensing.license$);
const securityFeature = license.getFeature('security');
// If security is disabled or unavailable, return true.
if (!securityFeature || !(securityFeature.isAvailable && securityFeature.isEnabled)) {
return true;
}
const names = [dataStreamName];
const resp = await esClient.security.hasPrivileges({
body: {
cluster: ['manage', 'cancel_task'],
index: [
{
names,
allow_restricted_indices: true,
privileges: ['all'],
},
],
},
});
return resp.has_all_requested;
},
async detectReindexWarnings(): Promise<DataStreamReindexWarning[]> {
return [
{
warningType: 'incompatibleDataStream',
},
];
},
async createReindexOperation(dataStreamName: string) {
const indexExists = await esClient.indices.exists({ index: dataStreamName });
if (!indexExists) {
throw error.indexNotFound(`Index ${dataStreamName} does not exist in this cluster.`);
}
try {
const result = await esClient.transport.request<{ acknowledged: boolean }>({
method: 'POST',
path: '/_migration/reindex',
body: {
mode: 'upgrade',
source: {
index: dataStreamName,
},
},
});
if (!result.acknowledged) {
throw error.reindexTaskFailed(
`The reindex operation failed to start for ${dataStreamName}`
);
}
return true;
} catch (err) {
if (err.status === 400 && err.error.type === 'resource_already_exists_exception') {
throw error.reindexAlreadyInProgress(
`A reindex operation already in-progress for ${dataStreamName}`
);
}
throw error.reindexTaskFailed(
`The reindex operation failed to start for ${dataStreamName}`
);
}
},
async fetchReindexStatus(dataStreamName: string): Promise<DataStreamReindexOperation> {
// Check reindexing task progress
try {
const taskResponse = await esClient.transport.request<DataStreamReindexTaskStatusResponse>({
method: 'GET',
path: `/_migration/reindex/${dataStreamName}/_status`,
});
if (taskResponse.exception) {
// 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(
`Data Stream Reindexing exception:\n${taskResponse.exception}\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
)}`
);
}
// Update the status
return {
reindexTaskPercComplete: 1,
status: DataStreamReindexStatus.completed,
progressDetails: {
startTimeMs: taskResponse.start_time_millis,
successCount: taskResponse.successes,
pendingCount: taskResponse.pending,
inProgressCount: (taskResponse.in_progress ?? []).length,
errorsCount: (taskResponse.errors ?? []).length,
},
};
} else {
// Updated the percent complete
const perc = taskResponse.successes / taskResponse.total_indices_in_data_stream;
return {
status: DataStreamReindexStatus.inProgress,
reindexTaskPercComplete: perc,
progressDetails: {
startTimeMs: taskResponse.start_time_millis,
successCount: taskResponse.successes,
pendingCount: taskResponse.pending,
inProgressCount: (taskResponse.in_progress ?? []).length,
errorsCount: (taskResponse.errors ?? []).length,
},
};
}
} catch (err) {
if (
err.name === 'ResponseError' &&
(err.message as string).includes('resource_not_found_exception')
) {
// 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,
};
}
return {
status: DataStreamReindexStatus.failed,
errorMessage: err.toString(),
};
}
},
async cancelReindexing(dataStreamName: string) {
const resp = await esClient.transport.request<{ acknowledged: boolean }>({
method: 'POST',
path: `/_migration/reindex/${dataStreamName}/_cancel`,
});
if (!resp.acknowledged) {
throw error.reindexCannotBeCancelled(`Could not cancel reindex.`);
}
return {
status: DataStreamReindexStatus.cancelled,
};
},
async getDataStreamMetadata(dataStreamName: string): Promise<DataStreamMetadata | null> {
try {
const { body: statsBody } = (await esClient.transport.request(
{
method: 'GET',
path: `/${dataStreamName}/_stats`,
},
{ meta: true }
)) as TransportResult<any>;
const { data_streams: dataStreamsDeprecations } = await esClient.migration.deprecations({
filter_path: `data_streams`,
});
const deprecationsDetails = dataStreamsDeprecations[dataStreamName];
if (!deprecationsDetails || !deprecationsDetails.length) {
return null;
}
// Find the first deprecation that has reindex_required set to true
const deprecationDetails = deprecationsDetails.find(
(deprecation) => deprecation._meta!.reindex_required
);
if (!deprecationDetails) {
return null;
}
const indicesRequiringUpgrade: string[] =
deprecationDetails._meta!.indices_requiring_upgrade;
const allIndices = Object.keys(statsBody.indices);
let indicesRequiringUpgradeDocsCount = 0;
let indicesRequiringUpgradeDocsSize = 0;
const indicesCreationDates = [];
for (const index of indicesRequiringUpgrade) {
const indexStats = Object.entries(statsBody.indices).find(([key]) => key === index);
if (!indexStats) {
throw error.cannotGrabMetadata(`Index ${index} does not exist in this cluster.`);
}
indicesRequiringUpgradeDocsSize += (indexStats[1] as any).total.store
.total_data_set_size_in_bytes;
indicesRequiringUpgradeDocsCount += (indexStats[1] as any).total.docs.count;
const body = await esClient.indices.getSettings({
index,
flat_settings: true,
});
const creationDate = _.get(body, [index, 'settings', 'index.creation_date']);
if (creationDate) {
indicesCreationDates.push(creationDate);
}
}
const lastIndexRequiringUpgradeCreationDate = Math.max(...indicesCreationDates);
return {
dataStreamName,
documentationUrl: deprecationDetails.url,
allIndices,
allIndicesCount: allIndices.length,
indicesRequiringUpgrade,
indicesRequiringUpgradeCount: indicesRequiringUpgrade.length,
lastIndexRequiringUpgradeCreationDate,
indicesRequiringUpgradeDocsSize,
indicesRequiringUpgradeDocsCount,
};
} catch (err) {
throw error.cannotGrabMetadata(
`Could not grab metadata for ${dataStreamName}. ${err.message.toString()}`
);
}
},
};
};

View file

@ -0,0 +1,34 @@
/*
* 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 {
AccessForbidden,
IndexNotFound,
ReindexTaskFailed,
ReindexAlreadyInProgress,
ReindexCannotBeCancelled,
MetadataCannotBeGrabbed,
} from './error_symbols';
export class ReindexError extends Error {
constructor(message: string, public readonly symbol: symbol) {
super(message);
}
}
export const createErrorFactory = (symbol: symbol) => (message: string) => {
return new ReindexError(message, symbol);
};
export const error = {
indexNotFound: createErrorFactory(IndexNotFound),
accessForbidden: createErrorFactory(AccessForbidden),
cannotGrabMetadata: createErrorFactory(MetadataCannotBeGrabbed),
reindexTaskFailed: createErrorFactory(ReindexTaskFailed),
reindexAlreadyInProgress: createErrorFactory(ReindexAlreadyInProgress),
reindexCannotBeCancelled: createErrorFactory(ReindexCannotBeCancelled),
};

View file

@ -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 const AccessForbidden = Symbol('AccessForbidden');
export const IndexNotFound = Symbol('IndexNotFound');
export const ReindexTaskFailed = Symbol('ReindexTaskFailed');
export const ReindexAlreadyInProgress = Symbol('ReindexAlreadyInProgress');
export const ReindexCannotBeCancelled = Symbol('ReindexCannotBeCancelled');
export const MetadataCannotBeGrabbed = Symbol('MetadataCannotBeGrabbed');

View file

@ -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 { dataStreamReindexServiceFactory } from './data_stream_reindex_service';

View file

@ -13,12 +13,29 @@ interface Action {
}
interface Actions {
actions: Action[];
actions?: Action[];
}
export type EsMetadata = Actions & {
[key: string]: string;
};
interface MlActionMetadata {
actions?: Action[];
snapshot_id: string;
job_id: string;
}
interface DataStreamActionMetadata {
actions?: Action[];
total_backing_indices: number;
reindex_required: boolean;
// Action required before moving to 9.0
indices_requiring_upgrade_count?: number;
indices_requiring_upgrade?: string[];
// Action not required before moving to 9.0
ignored_indices_requiring_upgrade?: string[];
ignored_indices_requiring_upgrade_count?: number;
}
export type EsMetadata = Actions | 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 = [
@ -27,6 +44,7 @@ const ES_INDEX_MESSAGES_REQIURING_REINDEX = [
];
export const getCorrectiveAction = (
deprecationType: EnrichedDeprecationInfo['type'],
message: string,
metadata: EsMetadata,
indexName?: string
@ -43,6 +61,40 @@ export const getCorrectiveAction = (
const requiresIndexSettingsAction = Boolean(indexSettingDeprecation);
const requiresClusterSettingsAction = Boolean(clusterSettingDeprecation);
const requiresMlAction = /[Mm]odel snapshot/.test(message);
const requiresDataStreamsAction = deprecationType === 'data_streams';
if (requiresDataStreamsAction) {
const {
total_backing_indices: totalBackingIndices,
indices_requiring_upgrade_count: indicesRequiringUpgradeCount = 0,
indices_requiring_upgrade: indicesRequiringUpgrade = [],
ignored_indices_requiring_upgrade: ignoredIndicesRequiringUpgrade = [],
ignored_indices_requiring_upgrade_count: ignoredIndicesRequiringUpgradeCount = 0,
reindex_required: reindexRequired,
} = metadata as DataStreamActionMetadata;
/**
* If there are no indices requiring upgrade, or reindexRequired = false.
* Then we don't need to show the corrective action
*/
if (indicesRequiringUpgradeCount < 1 || !reindexRequired) {
return;
}
return {
type: 'dataStream',
metadata: {
ignoredIndicesRequiringUpgrade,
ignoredIndicesRequiringUpgradeCount,
totalBackingIndices,
indicesRequiringUpgradeCount,
indicesRequiringUpgrade,
reindexRequired,
},
};
}
if (requiresReindexAction) {
return {
@ -65,7 +117,7 @@ export const getCorrectiveAction = (
}
if (requiresMlAction) {
const { snapshot_id: snapshotId, job_id: jobId } = metadata!;
const { snapshot_id: snapshotId, job_id: jobId } = metadata as MlActionMetadata;
return {
type: 'mlSnapshot',

View file

@ -161,9 +161,10 @@ export const getEnrichedDeprecations = async (
})
.map((deprecation) => {
const correctiveAction = getCorrectiveAction(
deprecation.type,
deprecation.message,
deprecation.metadata as EsMetadata,
deprecation.index!
deprecation.index
);
// If we have found deprecation information for index/indices

View file

@ -21,9 +21,11 @@ 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';
export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) {
registerAppRoutes(dependencies);
registerCloudBackupStatusRoutes(dependencies);
registerClusterUpgradeStatusRoutes(dependencies);
registerSystemIndicesMigrationRoutes(dependencies);
@ -38,4 +40,7 @@ export function registerRoutes(dependencies: RouteDependencies, getWorker: () =>
registerRemoteClustersRoute(dependencies);
registerNodeDiskSpaceRoute(dependencies);
registerClusterSettingsRoute(dependencies);
// Data streams reindexing
registerReindexDataStreamRoutes(dependencies);
}

View file

@ -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 { registerReindexDataStreamRoutes } from './reindex_data_stream';

View file

@ -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 { kibanaResponseFactory } from '@kbn/core/server';
import {
AccessForbidden,
IndexNotFound,
ReindexAlreadyInProgress,
ReindexCannotBeCancelled,
ReindexTaskFailed,
MetadataCannotBeGrabbed,
} from '../../lib/data_streams/error_symbols';
import { ReindexError } from '../../lib/data_streams/error';
export const mapAnyErrorToKibanaHttpResponse = (e: any) => {
if (e instanceof ReindexError) {
switch (e.symbol) {
case AccessForbidden:
return kibanaResponseFactory.forbidden({ body: e.message });
case IndexNotFound:
return kibanaResponseFactory.notFound({ body: e.message });
case ReindexTaskFailed:
// Bad data
return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 });
case ReindexAlreadyInProgress:
case ReindexCannotBeCancelled:
case MetadataCannotBeGrabbed:
return kibanaResponseFactory.badRequest({ body: e.message });
default:
// nothing matched
}
}
throw e;
};

View file

@ -0,0 +1,238 @@
/*
* 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 { i18n } from '@kbn/i18n';
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 { RouteDependencies } from '../../types';
import { mapAnyErrorToKibanaHttpResponse } from './map_any_error_to_kibana_http_response';
export function registerReindexDataStreamRoutes({
router,
licensing,
log,
getSecurityPlugin,
lib: { handleEsError },
}: RouteDependencies) {
const BASE_PATH = `${API_BASE_PATH}/reindex_data_streams`;
router.post(
{
path: `${BASE_PATH}/{dataStreamName}`,
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(),
}),
},
},
versionCheckHandlerWrapper(async ({ core }, request, response) => {
const {
elasticsearch: { client: esClient },
} = await core;
const { dataStreamName } = request.params;
try {
const callAsCurrentUser = esClient.asCurrentUser;
const reindexService = dataStreamReindexServiceFactory({
esClient: callAsCurrentUser,
log,
licensing,
});
if (!(await reindexService.hasRequiredPrivileges(dataStreamName))) {
throw error.accessForbidden(
i18n.translate(
'xpack.upgradeAssistant.datastream.reindex.reindexPrivilegesErrorBatch',
{
defaultMessage: `You do not have adequate privileges to reindex "{dataStreamName}".`,
values: { dataStreamName },
}
)
);
}
await reindexService.createReindexOperation(dataStreamName);
return response.ok();
} catch (err) {
if (err instanceof errors.ResponseError) {
return handleEsError({ error: err, response });
}
return mapAnyErrorToKibanaHttpResponse(err);
}
})
);
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 reindexService = dataStreamReindexServiceFactory({
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,
};
return response.ok({
body,
});
} catch (err) {
if (err instanceof errors.ResponseError) {
return handleEsError({ error: err, response });
}
return mapAnyErrorToKibanaHttpResponse(error);
}
})
);
router.get(
{
path: `${BASE_PATH}/{dataStreamName}/metadata`,
options: {
access: 'public',
summary: `Get data stream reindexing metadata`,
},
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 reindexService = dataStreamReindexServiceFactory({
esClient: asCurrentUser,
log,
licensing,
});
try {
const dataStreamMetadata = await reindexService.getDataStreamMetadata(dataStreamName);
return response.ok({
body: dataStreamMetadata || undefined,
});
} catch (err) {
if (err instanceof errors.ResponseError) {
return handleEsError({ error: err, response });
}
return mapAnyErrorToKibanaHttpResponse(error);
}
})
);
router.post(
{
path: `${BASE_PATH}/{dataStreamName}/cancel`,
security: {
authz: {
enabled: false,
reason: 'Relies on elasticsearch for authorization',
},
},
options: {
access: 'public',
summary: `Cancel Data Stream reindexing`,
},
validate: {
params: schema.object({
dataStreamName: schema.string(),
}),
},
},
versionCheckHandlerWrapper(async ({ core }, request, response) => {
const {
elasticsearch: { client: esClient },
} = await core;
const { dataStreamName } = request.params;
const callAsCurrentUser = esClient.asCurrentUser;
const reindexService = dataStreamReindexServiceFactory({
esClient: callAsCurrentUser,
log,
licensing,
});
try {
if (!(await reindexService.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 },
}
)
);
}
await reindexService.cancelReindexing(dataStreamName);
return response.ok({ body: { acknowledged: true } });
} catch (err) {
if (err instanceof errors.ResponseError) {
return handleEsError({ error: err, response });
}
return mapAnyErrorToKibanaHttpResponse(error);
}
})
);
}