[ML] Transforms: Add ability to delete dest index & index pattern when deleting transform job (#67922)

This commit is contained in:
Quynh Nguyen 2020-06-10 10:11:46 -05:00 committed by GitHub
parent 8118b13ff7
commit 9bc0936311
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1165 additions and 107 deletions

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
BoomResponse,
extractErrorMessage,
MLCustomHttpResponseOptions,
MLResponseError,
} from './errors';
import { ResponseError } from 'kibana/server';
describe('ML - error message utils', () => {
describe('extractErrorMessage', () => {
test('returns just the error message', () => {
const testMsg = 'Saved object [index-pattern/blahblahblah] not found';
const bodyWithNestedErrorMsg: MLCustomHttpResponseOptions<MLResponseError> = {
body: {
message: {
msg: testMsg,
},
},
statusCode: 404,
};
expect(extractErrorMessage(bodyWithNestedErrorMsg)).toBe(testMsg);
const bodyWithStringMsg: MLCustomHttpResponseOptions<MLResponseError> = {
body: {
msg: testMsg,
},
statusCode: 404,
};
expect(extractErrorMessage(bodyWithStringMsg)).toBe(testMsg);
const bodyWithStringMessage: MLCustomHttpResponseOptions<ResponseError> = {
body: {
message: testMsg,
},
statusCode: 404,
};
expect(extractErrorMessage(bodyWithStringMessage)).toBe(testMsg);
const bodyWithString: MLCustomHttpResponseOptions<ResponseError> = {
body: testMsg,
statusCode: 404,
};
expect(extractErrorMessage(bodyWithString)).toBe(testMsg);
const bodyWithError: MLCustomHttpResponseOptions<ResponseError> = {
body: new Error(testMsg),
statusCode: 404,
};
expect(extractErrorMessage(bodyWithError)).toBe(testMsg);
const bodyWithBoomError: MLCustomHttpResponseOptions<BoomResponse> = {
statusCode: 404,
body: {
data: [],
isBoom: true,
isServer: false,
output: {
statusCode: 404,
payload: {
statusCode: 404,
error: testMsg,
message: testMsg,
},
headers: {},
},
},
};
expect(extractErrorMessage(bodyWithBoomError)).toBe(testMsg);
});
});
});

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ResponseError, ResponseHeaders } from 'kibana/server';
import { isErrorResponse } from '../types/errors';
export function getErrorMessage(error: any) {
@ -17,3 +18,77 @@ export function getErrorMessage(error: any) {
return JSON.stringify(error);
}
// Adding temporary types until Kibana ResponseError is updated
export interface BoomResponse {
data: any;
isBoom: boolean;
isServer: boolean;
output: {
statusCode: number;
payload: {
statusCode: number;
error: string;
message: string;
};
headers: {};
};
}
export type MLResponseError =
| {
message: {
msg: string;
};
}
| { msg: string };
export interface MLCustomHttpResponseOptions<
T extends ResponseError | MLResponseError | BoomResponse
> {
/** HTTP message to send to the client */
body?: T;
/** HTTP Headers with additional information about response */
headers?: ResponseHeaders;
statusCode: number;
}
export const extractErrorMessage = (
error:
| MLCustomHttpResponseOptions<MLResponseError | ResponseError | BoomResponse>
| undefined
| string
): string => {
// extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages
if (typeof error === 'string') {
return error;
}
if (error?.body === undefined) return '';
if (typeof error.body === 'string') {
return error.body;
}
if (
typeof error.body === 'object' &&
'output' in error.body &&
error.body.output.payload.message
) {
return error.body.output.payload.message;
}
if (typeof error.body === 'object' && 'msg' in error.body && typeof error.body.msg === 'string') {
return error.body.msg;
}
if (typeof error.body === 'object' && 'message' in error.body) {
if (typeof error.body.message === 'string') {
return error.body.message;
}
if (!(error.body.message instanceof Error) && typeof (error.body.message.msg === 'string')) {
return error.body.message.msg;
}
}
// If all else fail return an empty message instead of JSON.stringify
return '';
};

View file

@ -18,6 +18,7 @@ import {
} from '@elastic/eui';
import { IIndexPattern } from 'src/plugins/data/common';
import { FormattedMessage } from '@kbn/i18n/react';
import { extractErrorMessage } from '../../../../../../../common/util/errors';
import {
deleteAnalytics,
deleteAnalyticsAndDestIndex,
@ -29,7 +30,6 @@ import {
} from '../../../../../capabilities/check_capabilities';
import { useMlKibana } from '../../../../../contexts/kibana';
import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
import { extractErrorMessage } from '../../../../../util/error_utils';
interface DeleteActionProps {
item: DataFrameAnalyticsListRow;

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { extractErrorMessage } from '../../../../../../../common/util/errors';
import { getToastNotifications } from '../../../../../util/dependency_cache';
import { ml } from '../../../../../services/ml_api_service';
import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common';
@ -11,7 +12,6 @@ import {
isDataFrameAnalyticsFailed,
DataFrameAnalyticsListRow,
} from '../../components/analytics_list/common';
import { extractErrorMessage } from '../../../../../util/error_utils';
export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => {
const toastNotifications = getToastNotifications();

View file

@ -1,32 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CustomHttpResponseOptions, ResponseError } from 'kibana/server';
export const extractErrorMessage = (
error: CustomHttpResponseOptions<ResponseError> | undefined | string
): string | undefined => {
if (typeof error === 'string') {
return error;
}
if (error?.body) {
if (typeof error.body === 'string') {
return error.body;
}
if (typeof error.body === 'object' && 'message' in error.body) {
if (typeof error.body.message === 'string') {
return error.body.message;
}
// @ts-ignore
if (typeof (error.body.message?.msg === 'string')) {
// @ts-ignore
return error.body.message?.msg;
}
}
}
return undefined;
};

View file

@ -14,6 +14,7 @@ export * from '../common/types/audit_message';
export * from '../common/util/anomaly_utils';
export * from '../common/util/errors';
export * from '../common/util/validators';
export * from './application/formatters/metric_change_description';

View file

@ -38,3 +38,20 @@ export interface ResultData {
export interface TransformEndpointResult {
[key: string]: ResultData;
}
export interface DeleteTransformEndpointRequest {
transformsInfo: TransformEndpointRequest[];
deleteDestIndex?: boolean;
deleteDestIndexPattern?: boolean;
}
export interface DeleteTransformStatus {
transformDeleted: ResultData;
destIndexDeleted?: ResultData;
destIndexPatternDeleted?: ResultData;
destinationIndex?: string | undefined;
}
export interface DeleteTransformEndpointResult {
[key: string]: DeleteTransformStatus;
}

View file

@ -29,9 +29,14 @@ const MAX_SIMPLE_MESSAGE_LENGTH = 140;
interface ToastNotificationTextProps {
overlays: CoreStart['overlays'];
text: any;
previewTextLength?: number;
}
export const ToastNotificationText: FC<ToastNotificationTextProps> = ({ overlays, text }) => {
export const ToastNotificationText: FC<ToastNotificationTextProps> = ({
overlays,
text,
previewTextLength,
}) => {
if (typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) {
return text;
}
@ -46,8 +51,9 @@ export const ToastNotificationText: FC<ToastNotificationTextProps> = ({ overlays
const unformattedText = text.message ? text.message : text;
const formattedText = typeof unformattedText === 'object' ? JSON.stringify(text, null, 2) : text;
const previewText = `${formattedText.substring(0, 140)}${
formattedText.length > 140 ? ' ...' : ''
const textLength = previewTextLength ?? 140;
const previewText = `${formattedText.substring(0, textLength)}${
formattedText.length > textLength ? ' ...' : ''
}`;
const openModal = () => {

View file

@ -6,7 +6,7 @@
export { useApi } from './use_api';
export { useGetTransforms } from './use_get_transforms';
export { useDeleteTransforms } from './use_delete_transform';
export { useDeleteTransforms, useDeleteIndexAndTargetIndex } from './use_delete_transform';
export { useStartTransforms } from './use_start_transform';
export { useStopTransforms } from './use_stop_transform';
export { useRequest } from './use_request';

View file

@ -5,7 +5,12 @@
*/
import { useMemo } from 'react';
import { TransformEndpointRequest, TransformEndpointResult, TransformId } from '../../../common';
import {
TransformId,
TransformEndpointRequest,
TransformEndpointResult,
DeleteTransformEndpointResult,
} from '../../../common';
import { API_BASE_PATH } from '../../../common/constants';
import { useAppDependencies } from '../app_dependencies';
@ -40,10 +45,12 @@ export const useApi = () => {
});
},
deleteTransforms(
transformsInfo: TransformEndpointRequest[]
): Promise<TransformEndpointResult> {
transformsInfo: TransformEndpointRequest[],
deleteDestIndex: boolean | undefined,
deleteDestIndexPattern: boolean | undefined
): Promise<DeleteTransformEndpointResult> {
return http.post(`${API_BASE_PATH}delete_transforms`, {
body: JSON.stringify(transformsInfo),
body: JSON.stringify({ transformsInfo, deleteDestIndex, deleteDestIndexPattern }),
});
},
getTransformsPreview(obj: PreviewRequestBody): Promise<GetTransformsResponse> {

View file

@ -4,52 +4,257 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
import { TransformEndpointRequest, TransformEndpointResult } from '../../../common';
import { getErrorMessage } from '../../shared_imports';
import {
TransformEndpointRequest,
DeleteTransformEndpointResult,
DeleteTransformStatus,
} from '../../../common';
import { getErrorMessage, extractErrorMessage } from '../../shared_imports';
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import { ToastNotificationText } from '../components';
import { useApi } from './use_api';
import { indexService } from '../services/es_index_service';
export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
const { http, savedObjects } = useAppDependencies();
const toastNotifications = useToastNotifications();
const [deleteDestIndex, setDeleteDestIndex] = useState<boolean>(true);
const [deleteIndexPattern, setDeleteIndexPattern] = useState<boolean>(true);
const [userCanDeleteIndex, setUserCanDeleteIndex] = useState<boolean>(false);
const [indexPatternExists, setIndexPatternExists] = useState<boolean>(false);
const toggleDeleteIndex = useCallback(() => setDeleteDestIndex(!deleteDestIndex), [
deleteDestIndex,
]);
const toggleDeleteIndexPattern = useCallback(() => setDeleteIndexPattern(!deleteIndexPattern), [
deleteIndexPattern,
]);
const checkIndexPatternExists = useCallback(
async (indexName: string) => {
try {
if (await indexService.indexPatternExists(savedObjects.client, indexName)) {
setIndexPatternExists(true);
}
} catch (e) {
const error = extractErrorMessage(e);
toastNotifications.addDanger(
i18n.translate(
'xpack.transform.deleteTransform.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage',
{
defaultMessage:
'An error occurred checking if index pattern {indexPattern} exists: {error}',
values: { indexPattern: indexName, error },
}
)
);
}
},
[savedObjects.client, toastNotifications]
);
const checkUserIndexPermission = useCallback(async () => {
try {
const userCanDelete = await indexService.canDeleteIndex(http);
if (userCanDelete) {
setUserCanDeleteIndex(true);
}
} catch (e) {
toastNotifications.addDanger(
i18n.translate(
'xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage',
{
defaultMessage: 'An error occurred checking if user can delete destination index',
}
)
);
}
}, [http, toastNotifications]);
useEffect(() => {
checkUserIndexPermission();
if (items.length === 1) {
const config = items[0].config;
const destinationIndex = Array.isArray(config.dest.index)
? config.dest.index[0]
: config.dest.index;
checkIndexPatternExists(destinationIndex);
} else {
setIndexPatternExists(true);
}
}, [checkIndexPatternExists, checkUserIndexPermission, items]);
return {
userCanDeleteIndex,
deleteDestIndex,
indexPatternExists,
deleteIndexPattern,
toggleDeleteIndex,
toggleDeleteIndexPattern,
};
};
type SuccessCountField = keyof Omit<DeleteTransformStatus, 'destinationIndex'>;
export const useDeleteTransforms = () => {
const { overlays } = useAppDependencies();
const toastNotifications = useToastNotifications();
const api = useApi();
return async (transforms: TransformListRow[]) => {
return async (
transforms: TransformListRow[],
shouldDeleteDestIndex: boolean,
shouldDeleteDestIndexPattern: boolean
) => {
const transformsInfo: TransformEndpointRequest[] = transforms.map((tf) => ({
id: tf.config.id,
state: tf.stats.state,
}));
try {
const results: TransformEndpointResult = await api.deleteTransforms(transformsInfo);
const results: DeleteTransformEndpointResult = await api.deleteTransforms(
transformsInfo,
shouldDeleteDestIndex,
shouldDeleteDestIndexPattern
);
const isBulk = Object.keys(results).length > 1;
const successCount: Record<SuccessCountField, number> = {
transformDeleted: 0,
destIndexDeleted: 0,
destIndexPatternDeleted: 0,
};
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', {
defaultMessage: 'Request to delete transform {transformId} acknowledged.',
values: { transformId },
})
);
const status = results[transformId];
const destinationIndex = status.destinationIndex;
// if we are only deleting one transform, show the success toast messages
if (!isBulk && status.transformDeleted) {
if (status.transformDeleted?.success) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', {
defaultMessage: 'Request to delete transform {transformId} acknowledged.',
values: { transformId },
})
);
}
if (status.destIndexDeleted?.success) {
toastNotifications.addSuccess(
i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage',
{
defaultMessage:
'Request to delete destination index {destinationIndex} acknowledged.',
values: { destinationIndex },
}
)
);
}
if (status.destIndexPatternDeleted?.success) {
toastNotifications.addSuccess(
i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage',
{
defaultMessage:
'Request to delete index pattern {destinationIndex} acknowledged.',
values: { destinationIndex },
}
)
);
}
} else {
toastNotifications.addDanger(
i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', {
(Object.keys(successCount) as SuccessCountField[]).forEach((key) => {
if (status[key]?.success) {
successCount[key] = successCount[key] + 1;
}
});
}
if (status.transformDeleted?.error) {
const error = extractErrorMessage(status.transformDeleted.error);
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', {
defaultMessage: 'An error occurred deleting the transform {transformId}',
values: { transformId },
})
);
}),
text: toMountPoint(
<ToastNotificationText previewTextLength={50} overlays={overlays} text={error} />
),
});
}
if (status.destIndexDeleted?.error) {
const error = extractErrorMessage(status.destIndexDeleted.error);
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage',
{
defaultMessage: 'An error occurred deleting destination index {destinationIndex}',
values: { destinationIndex },
}
),
text: toMountPoint(
<ToastNotificationText previewTextLength={50} overlays={overlays} text={error} />
),
});
}
if (status.destIndexPatternDeleted?.error) {
const error = extractErrorMessage(status.destIndexPatternDeleted.error);
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage',
{
defaultMessage: 'An error occurred deleting index pattern {destinationIndex}',
values: { destinationIndex },
}
),
text: toMountPoint(
<ToastNotificationText previewTextLength={50} overlays={overlays} text={error} />
),
});
}
}
}
// if we are deleting multiple transforms, combine the success messages
if (isBulk) {
if (successCount.transformDeleted > 0) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.bulkDeleteTransformSuccessMessage', {
defaultMessage:
'Successfully deleted {count} {count, plural, one {transform} other {transforms}}.',
values: { count: successCount.transformDeleted },
})
);
}
if (successCount.destIndexDeleted > 0) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage', {
defaultMessage:
'Successfully deleted {count} destination {count, plural, one {index} other {indices}}.',
values: { count: successCount.destIndexDeleted },
})
);
}
if (successCount.destIndexPatternDeleted > 0) {
toastNotifications.addSuccess(
i18n.translate(
'xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage',
{
defaultMessage:
'Successfully deleted {count} destination index {count, plural, one {pattern} other {patterns}}.',
values: { count: successCount.destIndexPatternDeleted },
}
)
);
}
}
@ -59,7 +264,13 @@ export const useDeleteTransforms = () => {
title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', {
defaultMessage: 'An error occurred calling the API endpoint to delete transforms.',
}),
text: toMountPoint(<ToastNotificationText overlays={overlays} text={getErrorMessage(e)} />),
text: toMountPoint(
<ToastNotificationText
previewTextLength={50}
overlays={overlays}
text={getErrorMessage(e)}
/>
),
});
}
};

View file

@ -12,11 +12,14 @@ import {
EuiOverlayMask,
EuiToolTip,
EUI_MODAL_CONFIRM_BUTTON,
EuiFlexGroup,
EuiFlexItem,
EuiSwitch,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { TRANSFORM_STATE } from '../../../../../../common';
import { useDeleteTransforms } from '../../../../hooks';
import { useDeleteTransforms, useDeleteIndexAndTargetIndex } from '../../../../hooks';
import {
createCapabilityFailureMessage,
AuthorizationContext,
@ -35,13 +38,25 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
const { canDeleteTransform } = useContext(AuthorizationContext).capabilities;
const deleteTransforms = useDeleteTransforms();
const {
userCanDeleteIndex,
deleteDestIndex,
indexPatternExists,
deleteIndexPattern,
toggleDeleteIndex,
toggleDeleteIndexPattern,
} = useDeleteIndexAndTargetIndex(items);
const [isModalVisible, setModalVisible] = useState(false);
const closeModal = () => setModalVisible(false);
const deleteAndCloseModal = () => {
setModalVisible(false);
deleteTransforms(items);
const shouldDeleteDestIndex = userCanDeleteIndex && deleteDestIndex;
const shouldDeleteDestIndexPattern =
userCanDeleteIndex && indexPatternExists && deleteIndexPattern;
deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern);
};
const openModal = () => setModalVisible(true);
@ -71,17 +86,96 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
defaultMessage: 'Delete {transformId}',
values: { transformId: items[0] && items[0].config.id },
});
const bulkDeleteModalMessage = i18n.translate(
'xpack.transform.transformList.bulkDeleteModalBody',
{
defaultMessage:
"Are you sure you want to delete {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}? The transform's destination index and optional Kibana index pattern will not be deleted.",
values: { count: items.length },
}
const bulkDeleteModalContent = (
<>
<p>
<FormattedMessage
id="xpack.transform.transformList.bulkDeleteModalBody"
defaultMessage="Are you sure you want to delete {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}?"
values={{ count: items.length }}
/>
</p>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
{
<EuiSwitch
data-test-subj="transformBulkDeleteIndexSwitch"
label={i18n.translate(
'xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle',
{
defaultMessage: 'Delete destination indices',
}
)}
checked={deleteDestIndex}
onChange={toggleDeleteIndex}
/>
}
</EuiFlexItem>
<EuiSpacer size="s" />
<EuiFlexItem>
{
<EuiSwitch
data-test-subj="transformBulkDeleteIndexPatternSwitch"
label={i18n.translate(
'xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle',
{
defaultMessage: 'Delete destination index patterns',
}
)}
checked={deleteIndexPattern}
onChange={toggleDeleteIndexPattern}
/>
}
</EuiFlexItem>
</EuiFlexGroup>
</>
);
const deleteModalContent = (
<>
<p>
<FormattedMessage
id="xpack.transform.transformList.deleteModalBody"
defaultMessage="Are you sure you want to delete this transform?"
/>
</p>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
{userCanDeleteIndex && (
<EuiSwitch
data-test-subj="transformDeleteIndexSwitch"
label={i18n.translate(
'xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle',
{
defaultMessage: 'Delete destination index {destinationIndex}',
values: { destinationIndex: items[0] && items[0].config.dest.index },
}
)}
checked={deleteDestIndex}
onChange={toggleDeleteIndex}
/>
)}
</EuiFlexItem>
{userCanDeleteIndex && indexPatternExists && (
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiSwitch
data-test-subj="transformDeleteIndexPatternSwitch"
label={i18n.translate(
'xpack.transform.actionDeleteTransform.deleteDestIndexPatternTitle',
{
defaultMessage: 'Delete index pattern {destinationIndex}',
values: { destinationIndex: items[0] && items[0].config.dest.index },
}
)}
checked={deleteIndexPattern}
onChange={toggleDeleteIndexPattern}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</>
);
const deleteModalMessage = i18n.translate('xpack.transform.transformList.deleteModalBody', {
defaultMessage: `Are you sure you want to delete this transform? The transform's destination index and optional Kibana index pattern will not be deleted.`,
});
let deleteButton = (
<EuiButtonEmpty
@ -99,7 +193,7 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
if (disabled || !canDeleteTransform) {
let content;
if (disabled) {
content = isBulkAction === true ? bulkDeleteButtonDisabledText : deleteButtonDisabledText;
content = isBulkAction ? bulkDeleteButtonDisabledText : deleteButtonDisabledText;
} else {
content = createCapabilityFailureMessage('canDeleteTransform');
}
@ -117,7 +211,7 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
{isModalVisible && (
<EuiOverlayMask>
<EuiConfirmModal
title={isBulkAction === true ? bulkDeleteModalTitle : deleteModalTitle}
title={isBulkAction ? bulkDeleteModalTitle : deleteModalTitle}
onCancel={closeModal}
onConfirm={deleteAndCloseModal}
cancelButtonText={i18n.translate(
@ -135,7 +229,7 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
buttonColor="danger"
>
<p>{isBulkAction === true ? bulkDeleteModalMessage : deleteModalMessage}</p>
{isBulkAction ? bulkDeleteModalContent : deleteModalContent}
</EuiConfirmModal>
</EuiOverlayMask>
)}

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpSetup, SavedObjectsClientContract } from 'kibana/public';
import { API_BASE_PATH } from '../../../common/constants';
import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns';
export class IndexService {
async canDeleteIndex(http: HttpSetup) {
const privilege = await http.get(`${API_BASE_PATH}privileges`);
if (!privilege) {
return false;
}
return privilege.hasAllPrivileges;
}
async indexPatternExists(savedObjectsClient: SavedObjectsClientContract, indexName: string) {
const response = await savedObjectsClient.find<IIndexPattern>({
type: 'index-pattern',
perPage: 1,
search: `"${indexName}"`,
searchFields: ['title'],
fields: ['title'],
});
const ip = response.savedObjects.find((obj) => obj.attributes.title === indexName);
return ip !== undefined;
}
}
export const indexService = new IndexService();

View file

@ -15,6 +15,7 @@ export {
export {
getErrorMessage,
extractErrorMessage,
getDataGridSchemaFromKibanaFieldType,
getFieldsFromKibanaIndexPattern,
multiColumnSortFactory,

View file

@ -10,7 +10,11 @@ import { i18n } from '@kbn/i18n';
import { ResponseError, CustomHttpResponseOptions } from 'src/core/server';
import { TransformEndpointRequest, TransformEndpointResult } from '../../../common';
import {
TransformEndpointRequest,
TransformEndpointResult,
DeleteTransformEndpointResult,
} from '../../../common';
const REQUEST_TIMEOUT = 'RequestTimeout';
@ -19,7 +23,7 @@ export function isRequestTimeout(error: any) {
}
interface Params {
results: TransformEndpointResult;
results: TransformEndpointResult | DeleteTransformEndpointResult;
id: string;
items: TransformEndpointRequest[];
action: string;
@ -59,7 +63,7 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params)
},
};
const newResults: TransformEndpointResult = {};
const newResults: TransformEndpointResult | DeleteTransformEndpointResult = {};
return items.reduce((accumResults, currentVal) => {
if (results[currentVal.id] === undefined) {

View file

@ -14,3 +14,17 @@ export const schemaTransformId = {
export interface SchemaTransformId {
transformId: string;
}
export const deleteTransformSchema = schema.object({
/**
* Delete Transform & Destination Index
*/
transformsInfo: schema.arrayOf(
schema.object({
id: schema.string(),
state: schema.maybe(schema.string()),
})
),
deleteDestIndex: schema.maybe(schema.boolean()),
deleteDestIndexPattern: schema.maybe(schema.boolean()),
});

View file

@ -5,7 +5,12 @@
*/
import { schema } from '@kbn/config-schema';
import { RequestHandler } from 'kibana/server';
import {
KibanaResponseFactory,
RequestHandler,
RequestHandlerContext,
SavedObjectsClientContract,
} from 'kibana/server';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers';
@ -14,6 +19,9 @@ import {
TransformEndpointResult,
TransformId,
TRANSFORM_STATE,
DeleteTransformEndpointRequest,
DeleteTransformStatus,
ResultData,
} from '../../../common';
import { RouteDependencies } from '../../types';
@ -21,8 +29,9 @@ import { RouteDependencies } from '../../types';
import { addBasePath } from '../index';
import { isRequestTimeout, fillResultsWithTimeouts, wrapError } from './error_utils';
import { schemaTransformId, SchemaTransformId } from './schema';
import { deleteTransformSchema, schemaTransformId, SchemaTransformId } from './schema';
import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messages';
import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns';
enum TRANSFORM_ACTIONS {
STOP = 'stop',
@ -173,15 +182,37 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) {
{
path: addBasePath('delete_transforms'),
validate: {
body: schema.maybe(schema.any()),
body: deleteTransformSchema,
},
},
license.guardApiRoute(async (ctx, req, res) => {
const transformsInfo = req.body as TransformEndpointRequest[];
const {
transformsInfo,
deleteDestIndex,
deleteDestIndexPattern,
} = req.body as DeleteTransformEndpointRequest;
try {
const body = await deleteTransforms(
transformsInfo,
deleteDestIndex,
deleteDestIndexPattern,
ctx,
license,
res
);
if (body && body.status) {
if (body.status === 404) {
return res.notFound();
}
if (body.status === 403) {
return res.forbidden();
}
}
return res.ok({
body: await deleteTransforms(transformsInfo, ctx.transform!.dataClient.callAsCurrentUser),
body,
});
} catch (e) {
return res.customError(wrapError(wrapEsError(e)));
@ -238,18 +269,51 @@ const getTransforms = async (options: { transformId?: string }, callAsCurrentUse
return await callAsCurrentUser('transform.getTransforms', options);
};
async function getIndexPatternId(
indexName: string,
savedObjectsClient: SavedObjectsClientContract
) {
const response = await savedObjectsClient.find<IIndexPattern>({
type: 'index-pattern',
perPage: 1,
search: `"${indexName}"`,
searchFields: ['title'],
fields: ['title'],
});
const ip = response.saved_objects.find((obj) => obj.attributes.title === indexName);
return ip?.id;
}
async function deleteDestIndexPatternById(
indexPatternId: string,
savedObjectsClient: SavedObjectsClientContract
) {
return await savedObjectsClient.delete('index-pattern', indexPatternId);
}
async function deleteTransforms(
transformsInfo: TransformEndpointRequest[],
callAsCurrentUser: CallCluster
deleteDestIndex: boolean | undefined,
deleteDestIndexPattern: boolean | undefined,
ctx: RequestHandlerContext,
license: RouteDependencies['license'],
response: KibanaResponseFactory
) {
const results: TransformEndpointResult = {};
const tempResults: TransformEndpointResult = {};
const results: Record<string, DeleteTransformStatus> = {};
for (const transformInfo of transformsInfo) {
let destinationIndex: string | undefined;
const transformDeleted: ResultData = { success: false };
const destIndexDeleted: ResultData = { success: false };
const destIndexPatternDeleted: ResultData = {
success: false,
};
const transformId = transformInfo.id;
try {
if (transformInfo.state === TRANSFORM_STATE.FAILED) {
try {
await callAsCurrentUser('transform.stopTransform', {
await ctx.transform!.dataClient.callAsCurrentUser('transform.stopTransform', {
transformId,
force: true,
waitForCompletion: true,
@ -257,7 +321,7 @@ async function deleteTransforms(
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
results: tempResults,
id: transformId,
items: transformsInfo,
action: TRANSFORM_ACTIONS.DELETE,
@ -265,9 +329,75 @@ async function deleteTransforms(
}
}
}
// Grab destination index info to delete
try {
const transformConfigs = await getTransforms(
{ transformId },
ctx.transform!.dataClient.callAsCurrentUser
);
const transformConfig = transformConfigs.transforms[0];
destinationIndex = Array.isArray(transformConfig.dest.index)
? transformConfig.dest.index[0]
: transformConfig.dest.index;
} catch (getTransformConfigError) {
transformDeleted.error = wrapError(getTransformConfigError);
results[transformId] = {
transformDeleted,
destIndexDeleted,
destIndexPatternDeleted,
destinationIndex,
};
continue;
}
await callAsCurrentUser('transform.deleteTransform', { transformId });
results[transformId] = { success: true };
// If user checks box to delete the destinationIndex associated with the job
if (destinationIndex && deleteDestIndex) {
try {
// If user does have privilege to delete the index, then delete the index
// if no permission then return 403 forbidden
await ctx.transform!.dataClient.callAsCurrentUser('indices.delete', {
index: destinationIndex,
});
destIndexDeleted.success = true;
} catch (deleteIndexError) {
destIndexDeleted.error = wrapError(deleteIndexError);
}
}
// Delete the index pattern if there's an index pattern that matches the name of dest index
if (destinationIndex && deleteDestIndexPattern) {
try {
const indexPatternId = await getIndexPatternId(
destinationIndex,
ctx.core.savedObjects.client
);
if (indexPatternId) {
await deleteDestIndexPatternById(indexPatternId, ctx.core.savedObjects.client);
destIndexPatternDeleted.success = true;
}
} catch (deleteDestIndexPatternError) {
destIndexPatternDeleted.error = wrapError(deleteDestIndexPatternError);
}
}
try {
await ctx.transform!.dataClient.callAsCurrentUser('transform.deleteTransform', {
transformId,
});
transformDeleted.success = true;
} catch (deleteTransformJobError) {
transformDeleted.error = wrapError(deleteTransformJobError);
if (transformDeleted.error.statusCode === 403) {
return response.forbidden();
}
}
results[transformId] = {
transformDeleted,
destIndexDeleted,
destIndexPatternDeleted,
destinationIndex,
};
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
@ -277,7 +407,7 @@ async function deleteTransforms(
action: TRANSFORM_ACTIONS.DELETE,
});
}
results[transformId] = { success: false, error: JSON.stringify(e) };
results[transformId] = { transformDeleted: { success: false, error: JSON.stringify(e) } };
}
}
return results;

View file

@ -30,5 +30,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./ingest_manager'));
loadTestFile(require.resolve('./endpoint'));
loadTestFile(require.resolve('./ml'));
loadTestFile(require.resolve('./transform'));
});
}

View file

@ -197,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => {
await ml.testResources.deleteIndexPattern(destinationIndex);
});
it('deletes job, target index, and index pattern by id', async () => {
it('should delete job, target index, and index pattern by id', async () => {
const { body } = await supertest
.delete(`/api/ml/data_frame/analytics/${analyticsId}`)
.query({ deleteDestIndex: true, deleteDestIndexPattern: true })

View file

@ -0,0 +1,318 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { TransformEndpointRequest } from '../../../../plugins/transform/common';
import { FtrProviderContext } from '../../ftr_provider_context';
import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common';
import { USER } from '../../../functional/services/transform/security_common';
async function asyncForEach(array: any[], callback: Function) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertestWithoutAuth');
const transform = getService('transform');
function generateDestIndex(transformId: string): string {
return `user-${transformId}`;
}
async function createTransform(transformId: string, destinationIndex: string) {
const config = {
id: transformId,
source: { index: ['farequote-*'] },
pivot: {
group_by: { airline: { terms: { field: 'airline' } } },
aggregations: { '@timestamp.value_count': { value_count: { field: '@timestamp' } } },
},
dest: { index: destinationIndex },
};
await transform.api.createTransform(config);
}
describe('delete_transforms', function () {
before(async () => {
await esArchiver.loadIfNeeded('ml/farequote');
await transform.testResources.setKibanaTimeZoneToUTC();
});
after(async () => {
await transform.api.cleanTransformIndices();
});
describe('single transform deletion', function () {
const transformId = 'test1';
const destinationIndex = generateDestIndex(transformId);
beforeEach(async () => {
await createTransform(transformId, destinationIndex);
await transform.api.createIndices(destinationIndex);
});
afterEach(async () => {
await transform.api.deleteIndices(destinationIndex);
});
it('should delete transform by transformId', async () => {
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
const { body } = await supertest
.post(`/api/transform/delete_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send({
transformsInfo,
})
.expect(200);
expect(body[transformId].transformDeleted.success).to.eql(true);
expect(body[transformId].destIndexDeleted.success).to.eql(false);
expect(body[transformId].destIndexPatternDeleted.success).to.eql(false);
await transform.api.waitForTransformNotToExist(transformId);
await transform.api.waitForIndicesToExist(destinationIndex);
});
it('should return 403 for unauthorized user', async () => {
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
await supertest
.post(`/api/transform/delete_transforms`)
.auth(
USER.TRANSFORM_VIEWER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER)
)
.set(COMMON_REQUEST_HEADERS)
.send({
transformsInfo,
})
.expect(403);
await transform.api.waitForTransformToExist(transformId);
await transform.api.waitForIndicesToExist(destinationIndex);
});
});
describe('single transform deletion with invalid transformId', function () {
it('should return 200 with error in response if invalid transformId', async () => {
const transformsInfo: TransformEndpointRequest[] = [{ id: 'invalid_transform_id' }];
const { body } = await supertest
.post(`/api/transform/delete_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send({
transformsInfo,
})
.expect(200);
expect(body.invalid_transform_id.transformDeleted.success).to.eql(false);
expect(body.invalid_transform_id.transformDeleted).to.have.property('error');
});
});
describe('bulk deletion', function () {
const transformsInfo: TransformEndpointRequest[] = [
{ id: 'bulk_delete_test_1' },
{ id: 'bulk_delete_test_2' },
];
const destinationIndices = transformsInfo.map((d) => generateDestIndex(d.id));
beforeEach(async () => {
await asyncForEach(transformsInfo, async ({ id }: { id: string }, idx: number) => {
await createTransform(id, destinationIndices[idx]);
await transform.api.createIndices(destinationIndices[idx]);
});
});
afterEach(async () => {
await asyncForEach(destinationIndices, async (destinationIndex: string) => {
await transform.api.deleteIndices(destinationIndex);
});
});
it('should delete multiple transforms by transformIds', async () => {
const { body } = await supertest
.post(`/api/transform/delete_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send({
transformsInfo,
})
.expect(200);
await asyncForEach(
transformsInfo,
async ({ id: transformId }: { id: string }, idx: number) => {
expect(body[transformId].transformDeleted.success).to.eql(true);
expect(body[transformId].destIndexDeleted.success).to.eql(false);
expect(body[transformId].destIndexPatternDeleted.success).to.eql(false);
await transform.api.waitForTransformNotToExist(transformId);
await transform.api.waitForIndicesToExist(destinationIndices[idx]);
}
);
});
it('should delete multiple transforms by transformIds, even if one of the transformIds is invalid', async () => {
const invalidTransformId = 'invalid_transform_id';
const { body } = await supertest
.post(`/api/transform/delete_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send({
transformsInfo: [
{ id: transformsInfo[0].id },
{ id: invalidTransformId },
{ id: transformsInfo[1].id },
],
})
.expect(200);
await asyncForEach(
transformsInfo,
async ({ id: transformId }: { id: string }, idx: number) => {
expect(body[transformId].transformDeleted.success).to.eql(true);
expect(body[transformId].destIndexDeleted.success).to.eql(false);
expect(body[transformId].destIndexPatternDeleted.success).to.eql(false);
await transform.api.waitForTransformNotToExist(transformId);
await transform.api.waitForIndicesToExist(destinationIndices[idx]);
}
);
expect(body[invalidTransformId].transformDeleted.success).to.eql(false);
expect(body[invalidTransformId].transformDeleted).to.have.property('error');
});
});
describe('with deleteDestIndex setting', function () {
const transformId = 'test2';
const destinationIndex = generateDestIndex(transformId);
before(async () => {
await createTransform(transformId, destinationIndex);
await transform.api.createIndices(destinationIndex);
});
after(async () => {
await transform.api.deleteIndices(destinationIndex);
});
it('should delete transform and destination index', async () => {
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
const { body } = await supertest
.post(`/api/transform/delete_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send({
transformsInfo,
deleteDestIndex: true,
})
.expect(200);
expect(body[transformId].transformDeleted.success).to.eql(true);
expect(body[transformId].destIndexDeleted.success).to.eql(true);
expect(body[transformId].destIndexPatternDeleted.success).to.eql(false);
await transform.api.waitForTransformNotToExist(transformId);
await transform.api.waitForIndicesNotToExist(destinationIndex);
});
});
describe('with deleteDestIndexPattern setting', function () {
const transformId = 'test3';
const destinationIndex = generateDestIndex(transformId);
before(async () => {
await createTransform(transformId, destinationIndex);
await transform.api.createIndices(destinationIndex);
await transform.testResources.createIndexPatternIfNeeded(destinationIndex);
});
after(async () => {
await transform.api.deleteIndices(destinationIndex);
await transform.testResources.deleteIndexPattern(destinationIndex);
});
it('should delete transform and destination index pattern', async () => {
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
const { body } = await supertest
.post(`/api/transform/delete_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send({
transformsInfo,
deleteDestIndex: false,
deleteDestIndexPattern: true,
})
.expect(200);
expect(body[transformId].transformDeleted.success).to.eql(true);
expect(body[transformId].destIndexDeleted.success).to.eql(false);
expect(body[transformId].destIndexPatternDeleted.success).to.eql(true);
await transform.api.waitForTransformNotToExist(transformId);
await transform.api.waitForIndicesToExist(destinationIndex);
await transform.testResources.assertIndexPatternNotExist(destinationIndex);
});
});
describe('with deleteDestIndex & deleteDestIndexPattern setting', function () {
const transformId = 'test4';
const destinationIndex = generateDestIndex(transformId);
before(async () => {
await createTransform(transformId, destinationIndex);
await transform.api.createIndices(destinationIndex);
await transform.testResources.createIndexPatternIfNeeded(destinationIndex);
});
after(async () => {
await transform.api.deleteIndices(destinationIndex);
await transform.testResources.deleteIndexPattern(destinationIndex);
});
it('should delete transform, destination index, & destination index pattern', async () => {
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
const { body } = await supertest
.post(`/api/transform/delete_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send({
transformsInfo,
deleteDestIndex: true,
deleteDestIndexPattern: true,
})
.expect(200);
expect(body[transformId].transformDeleted.success).to.eql(true);
expect(body[transformId].destIndexDeleted.success).to.eql(true);
expect(body[transformId].destIndexPatternDeleted.success).to.eql(true);
await transform.api.waitForTransformNotToExist(transformId);
await transform.api.waitForIndicesNotToExist(destinationIndex);
await transform.testResources.assertIndexPatternNotExist(destinationIndex);
});
});
});
};

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const transform = getService('transform');
describe('Machine Learning', function () {
this.tags(['transform']);
before(async () => {
await transform.securityCommon.createTransformRoles();
await transform.securityCommon.createTransformUsers();
});
after(async () => {
await transform.securityCommon.cleanTransformUsers();
await transform.securityCommon.cleanTransformRoles();
await esArchiver.unload('ml/farequote');
await transform.testResources.resetKibanaTimeZone();
});
loadTestFile(require.resolve('./delete_transforms'));
});
}

View file

@ -28,6 +28,7 @@ import { InfraLogSourceConfigurationProvider } from './infra_log_source_configur
import { MachineLearningProvider } from './ml';
import { IngestManagerProvider } from './ingest_manager';
import { ResolverGeneratorProvider } from './resolver';
import { TransformProvider } from './transform';
export const services = {
...commonServices,
@ -48,4 +49,5 @@ export const services = {
ml: MachineLearningProvider,
ingestManager: IngestManagerProvider,
resolverGenerator: ResolverGeneratorProvider,
transform: TransformProvider,
};

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrProviderContext } from '../../functional/ftr_provider_context';
import { TransformAPIProvider } from '../../functional/services/transform/api';
import { TransformSecurityCommonProvider } from '../../functional/services/transform/security_common';
import { MachineLearningTestResourcesProvider } from '../../functional/services/ml/test_resources';
export function TransformProvider(context: FtrProviderContext) {
const api = TransformAPIProvider(context);
const securityCommon = TransformSecurityCommonProvider(context);
const testResources = MachineLearningTestResourcesProvider(context);
return {
api,
securityCommon,
testResources,
};
}

View file

@ -20,6 +20,21 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
const esSupertest = getService('esSupertest');
return {
async createIndices(indices: string) {
log.debug(`Creating indices: '${indices}'...`);
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === true) {
log.debug(`Indices '${indices}' already exist. Nothing to create.`);
return;
}
const createResponse = await es.indices.create({ index: indices });
expect(createResponse)
.to.have.property('acknowledged')
.eql(true, 'Response for create request indices should be acknowledged.');
await this.waitForIndicesToExist(indices, `expected ${indices} to be created`);
},
async deleteIndices(indices: string) {
log.debug(`Deleting indices: '${indices}'...`);
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) {
@ -34,11 +49,25 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
.to.have.property('acknowledged')
.eql(true, 'Response for delete request should be acknowledged');
await retry.waitForWithTimeout(`'${indices}' indices to be deleted`, 30 * 1000, async () => {
await this.waitForIndicesNotToExist(indices, `expected indices '${indices}' to be deleted`);
},
async waitForIndicesToExist(indices: string, errorMsg?: string) {
await retry.tryForTime(30 * 1000, async () => {
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === true) {
return true;
} else {
throw new Error(errorMsg || `indices '${indices}' should exist`);
}
});
},
async waitForIndicesNotToExist(indices: string, errorMsg?: string) {
await retry.tryForTime(30 * 1000, async () => {
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) {
return true;
} else {
throw new Error(`expected indices '${indices}' to be deleted`);
throw new Error(errorMsg || `indices '${indices}' should not exist`);
}
});
},
@ -63,9 +92,7 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
async getTransformState(transformId: string): Promise<TRANSFORM_STATE> {
const stats = await this.getTransformStats(transformId);
const state: TRANSFORM_STATE = stats.state;
return state;
return stats.state;
},
async waitForTransformState(transformId: string, expectedState: TRANSFORM_STATE) {
@ -96,8 +123,8 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
});
},
async getTransform(transformId: string) {
return await esSupertest.get(`/_transform/${transformId}`).expect(200);
async getTransform(transformId: string, expectedCode = 200) {
return await esSupertest.get(`/_transform/${transformId}`).expect(expectedCode);
},
async createTransform(transformConfig: TransformPivotConfig) {
@ -105,11 +132,27 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
log.debug(`Creating transform with id '${transformId}'...`);
await esSupertest.put(`/_transform/${transformId}`).send(transformConfig).expect(200);
await retry.waitForWithTimeout(`'${transformId}' to be created`, 5 * 1000, async () => {
if (await this.getTransform(transformId)) {
await this.waitForTransformToExist(
transformId,
`expected transform '${transformId}' to be created`
);
},
async waitForTransformToExist(transformId: string, errorMsg?: string) {
await retry.waitForWithTimeout(`'${transformId}' to exist`, 5 * 1000, async () => {
if (await this.getTransform(transformId, 200)) {
return true;
} else {
throw new Error(`expected transform '${transformId}' to be created`);
throw new Error(errorMsg || `expected transform '${transformId}' to exist`);
}
});
},
async waitForTransformNotToExist(transformId: string, errorMsg?: string) {
await retry.waitForWithTimeout(`'${transformId}' to exist`, 5 * 1000, async () => {
if (await this.getTransform(transformId, 404)) {
return true;
} else {
throw new Error(errorMsg || `expected transform '${transformId}' to not exist`);
}
});
},