diff --git a/x-pack/plugins/cross_cluster_replication/fixtures/follower_index.js b/x-pack/plugins/cross_cluster_replication/fixtures/follower_index.js index e3639d0a9d738..ef888f8929a26 100644 --- a/x-pack/plugins/cross_cluster_replication/fixtures/follower_index.js +++ b/x-pack/plugins/cross_cluster_replication/fixtures/follower_index.js @@ -7,7 +7,7 @@ const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies const chance = new Chance(); -export const getFollowerIndexMock = ( +export const getFollowerIndexStatsMock = ( name = chance.string(), shards = [{ id: chance.string(), @@ -100,16 +100,59 @@ export const getFollowerIndexMock = ( }; }; -export const getFollowerIndexListMock = (total = 3) => { +export const getFollowerIndexListStatsMock = (total = 3, names) => { const list = { follow_stats: { indices: [], }, }; - let i = total; - while(i--) { - list.follow_stats.indices.push(getFollowerIndexMock()); + for(let i = 0; i < total; i++) { + list.follow_stats.indices.push(getFollowerIndexStatsMock(names[i])); + } + + return list; +}; + +export const getFollowerIndexInfoMock = ( + name = chance.string(), + status = chance.string(), + parameters = { + maxReadRequestOperationCount: chance.string(), + maxOutstandingReadRequests: chance.string(), + maxReadRequestSize: chance.string(), + maxWriteRequestOperationCount: chance.string(), + maxWriteRequestSize: chance.string(), + maxOutstandingWriteRequests: chance.string(), + maxWriteBufferCount: chance.string(), + maxWriteBufferSize: chance.string(), + maxRetryDelay: chance.string(), + readPollTimeout: chance.string(), + } +) => { + return { + follower_index: name, + status, + max_read_request_operation_count: parameters.maxReadRequestOperationCount, + max_outstanding_read_requests: parameters.maxOutstandingReadRequests, + max_read_request_size: parameters.maxReadRequestSize, + max_write_request_operation_count: parameters.maxWriteRequestOperationCount, + max_write_request_size: parameters.maxWriteRequestSize, + max_outstanding_write_requests: parameters.maxOutstandingWriteRequests, + max_write_buffer_count: parameters.maxWriteBufferCount, + max_write_buffer_size: parameters.maxWriteBufferSize, + max_retry_delay: parameters.maxRetryDelay, + read_poll_timeout: parameters.readPollTimeout, + }; +}; + +export const getFollowerIndexListInfoMock = (total = 3) => { + const list = { + follower_indices: [], + }; + + for(let i = 0; i < total; i++) { + list.follower_indices.push(getFollowerIndexInfoMock()); } return list; diff --git a/x-pack/plugins/cross_cluster_replication/fixtures/index.js b/x-pack/plugins/cross_cluster_replication/fixtures/index.js index fc1a2078ae5a9..9e76cf064118e 100644 --- a/x-pack/plugins/cross_cluster_replication/fixtures/index.js +++ b/x-pack/plugins/cross_cluster_replication/fixtures/index.js @@ -12,6 +12,8 @@ export { export { esErrors } from './es_errors'; export { - getFollowerIndexMock, - getFollowerIndexListMock, + getFollowerIndexStatsMock, + getFollowerIndexListStatsMock, + getFollowerIndexInfoMock, + getFollowerIndexListInfoMock, } from './follower_index'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.js index da0dfe854ba3a..e0fc691e84e3c 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.js @@ -8,6 +8,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { byteUnitsUrl, timeUnitsUrl } from '../../services/documentation_links'; +import { getSettingDefault } from '../../services/follower_index_default_settings'; const byteUnitsHelpText = ( {advancedSettingsFields.map((advancedSetting) => { - const { field, title, description, label, helpText } = advancedSetting; + const { field, title, description, label, helpText, defaultValue } = advancedSetting; return ( {title} )} - description={description} + description={( + + {description} + + + {' '} + {defaultValue} + + + )} label={label} helpText={helpText} areErrorsVisible={areErrorsVisible} diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js index 837d77a893f5a..fdd284641c800 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js @@ -5,6 +5,7 @@ */ import React, { PureComponent, Fragment } from 'react'; +import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { @@ -16,6 +17,10 @@ import { pauseFollowerIndex } from '../store/actions'; import { arrify } from '../../../common/services/utils'; class Provider extends PureComponent { + static propTypes = { + onConfirm: PropTypes.func, + } + state = { isModalOpen: false, ids: null @@ -34,6 +39,7 @@ class Provider extends PureComponent { onConfirm = () => { this.props.pauseFollowerIndex(this.state.ids); this.setState({ isModalOpen: false, ids: null }); + this.props.onConfirm && this.props.onConfirm(); } closeConfirmModal = () => { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js index d6bdeaca06755..7f8ca4ca65c5a 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js @@ -5,6 +5,7 @@ */ import React, { PureComponent, Fragment } from 'react'; +import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { @@ -16,6 +17,10 @@ import { resumeFollowerIndex } from '../store/actions'; import { arrify } from '../../../common/services/utils'; class Provider extends PureComponent { + static propTypes = { + onConfirm: PropTypes.func, + } + state = { isModalOpen: false, ids: null @@ -34,6 +39,7 @@ class Provider extends PureComponent { onConfirm = () => { this.props.resumeFollowerIndex(this.state.ids); this.setState({ isModalOpen: false, ids: null }); + this.props.onConfirm && this.props.onConfirm(); } closeConfirmModal = () => { @@ -78,12 +84,21 @@ class Provider extends PureComponent { } onMouseOver={this.onMouseOverModal} > - {!isSingle && ( + {isSingle ? ( +

+ +

+ ) : (

    {ids.map(id =>
  • {id}
  • )}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js index 86ac938582062..dc26655c74200 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js @@ -5,6 +5,7 @@ */ import React, { PureComponent, Fragment } from 'react'; +import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { @@ -16,6 +17,10 @@ import { unfollowLeaderIndex } from '../store/actions'; import { arrify } from '../../../common/services/utils'; class Provider extends PureComponent { + static propTypes = { + onConfirm: PropTypes.func, + } + state = { isModalOpen: false, ids: null @@ -34,6 +39,7 @@ class Provider extends PureComponent { onConfirm = () => { this.props.unfollowLeaderIndex(this.state.ids); this.setState({ isModalOpen: false, ids: null }); + this.props.onConfirm && this.props.onConfirm(); } closeConfirmModal = () => { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js index d800634e969cd..25f12267bfa33 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js @@ -148,7 +148,7 @@ export const FollowerIndexEdit = injectI18n( } renderConfirmModal = () => { - const { followerIndexId, intl } = this.props; + const { followerIndexId, intl, followerIndex: { isPaused } } = this.props; const title = intl.formatMessage({ id: 'xpack.crossClusterReplication.followerIndexEditForm.confirmModal.title', defaultMessage: 'Update follower index \'{id}\'?', @@ -166,19 +166,31 @@ export const FollowerIndexEdit = injectI18n( defaultMessage: 'Cancel', }) } - confirmButtonText={ - intl.formatMessage({ - id: 'xpack.crossClusterReplication.followerIndexEditForm.confirmModal.confirmButtonText', - defaultMessage: 'Update', - }) - } + confirmButtonText={isPaused ? ( + + ) : ( + + )} >

- + ) : ( + + /> + )}

diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js index fb58a451da5ff..fc1c55cb11261 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent } from 'react'; +import React, { PureComponent, Fragment } from 'react'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import PropTypes from 'prop-types'; import { @@ -83,10 +83,8 @@ export class ContextMenuUi extends PureComponent { ); - // TODO: Fix with correct logic when paused status info is available from ES, currently all - // follower indices are assumed to be active: https://github.com/elastic/elasticsearch/issues/37127 - const pausedFollowerIndexNames = followerIndices.filter(({ shards }) => !shards || !shards.length).map((index) => index.name); - const activeFollowerIndexNames = followerIndices.filter(({ shards }) => shards && shards.length).map((index) => index.name); + const pausedFollowerIndexNames = followerIndices.filter(({ isPaused }) => isPaused).map((index) => index.name); + const activeFollowerIndexNames = followerIndices.filter(({ isPaused }) => !isPaused).map((index) => index.name); return ( + {(pauseFollowerIndex) => ( + {(resumeFollowerIndex) => ( this.editFollowerIndex(followerIndexNames[0])} - > - - + + this.editFollowerIndex(followerIndexNames[0])} + > + + + ) } - + {(unfollowLeaderIndex) => ( + {value}{' '} + + + +
+ ); + } + return value; + } + renderFollowerIndex() { const { followerIndex: { name, remoteCluster, leaderIndex, + isPaused, shards, + maxReadRequestOperationCount, + maxOutstandingReadRequests, + maxReadRequestSize, + maxWriteRequestOperationCount, + maxWriteRequestSize, + maxOutstandingWriteRequests, + maxWriteBufferCount, + maxWriteBufferSize, + maxRetryDelay, + readPollTimeout, }, } = this.props; @@ -63,7 +93,7 @@ export class DetailPanelUi extends Component {

@@ -72,6 +102,39 @@ export class DetailPanelUi extends Component { + + + + + + + + + + {isPaused ? ( + + + + ) : ( + + + + )} + + + + + + @@ -104,6 +167,180 @@ export class DetailPanelUi extends Component { + {isPaused ? null : ( + + + + + + + + + + + + + {this.renderSetting('maxReadRequestOperationCount', maxReadRequestOperationCount)} + + + + + + + + + + + + {this.renderSetting('maxOutstandingReadRequests', maxOutstandingReadRequests)} + + + + + + + + + + + + + + + + {this.renderSetting('maxReadRequestSize', maxReadRequestSize)} + + + + + + + + + + + + {this.renderSetting('maxWriteRequestOperationCount', maxWriteRequestOperationCount)} + + + + + + + + + + + + + + + + {this.renderSetting('maxWriteRequestSize', maxWriteRequestSize)} + + + + + + + + + + + + {this.renderSetting('maxOutstandingWriteRequests', maxOutstandingWriteRequests)} + + + + + + + + + + + + + + + + {this.renderSetting('maxWriteBufferCount', maxWriteBufferCount)} + + + + + + + + + + + + {this.renderSetting('maxWriteBufferSize', maxWriteBufferSize)} + + + + + + + + + + + + + + + + {this.renderSetting('maxRetryDelay', maxRetryDelay)} + + + + + + + + + + + + {this.renderSetting('readPollTimeout', readPollTimeout)} + + + + + )} + - {shards.map((shard, i) => ( + {shards && shards.map((shard, i) => ( @@ -264,7 +501,7 @@ export class DetailPanelUi extends Component { onClose={closeDetailPanel} aria-labelledby="followerIndexDetailsFlyoutTitle" size="m" - maxWidth={400} + maxWidth={600} > diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index 7455866a07087..e15b665898416 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -8,6 +8,7 @@ import React, { PureComponent, Fragment } from 'react'; import PropTypes from 'prop-types'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { + EuiHealth, EuiIcon, EuiInMemoryTable, EuiLink, @@ -72,8 +73,7 @@ export const FollowerIndicesTable = injectI18n( const actions = [ /* Pause or resume follower index */ { - render: ({ name, shards }) => { - const isPaused = !shards || !shards.length; + render: ({ name, isPaused }) => { const label = isPaused ? intl.formatMessage({ id: 'xpack.crossClusterReplication.followerIndexList.table.actionResumeDescription', @@ -91,7 +91,6 @@ export const FollowerIndicesTable = injectI18n( {label} @@ -105,7 +104,6 @@ export const FollowerIndicesTable = injectI18n( {label} @@ -128,7 +126,6 @@ export const FollowerIndicesTable = injectI18n( {label} @@ -151,7 +148,6 @@ export const FollowerIndicesTable = injectI18n( {label} @@ -179,24 +175,28 @@ export const FollowerIndicesTable = injectI18n( ); } }, { - field: 'shards', + field: 'isPaused', name: intl.formatMessage({ id: 'xpack.crossClusterReplication.followerIndexList.table.statusColumnTitle', defaultMessage: 'Status', }), truncateText: true, sortable: true, - render: (shards) => { - return shards && shards.length ? ( - + render: (isPaused) => { + return isPaused ? ( + + + ) : ( - + + + ); } }, { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js new file mode 100644 index 0000000000000..094f7d6e957dc --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js @@ -0,0 +1,30 @@ +/* + * 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. + */ + +const defaultSettings = { + maxReadRequestOperationCount: 5120, + maxOutstandingReadRequests: 12, + maxReadRequestSize: '32mb', + maxWriteRequestOperationCount: 5120, + maxWriteRequestSize: '9223372036854775807b', + maxOutstandingWriteRequests: 9, + maxWriteBufferCount: 2147483647, + maxWriteBufferSize: '512mb', + maxRetryDelay: '500ms', + readPollTimeout: '1m', +}; + +export const getSettingDefault = (name) => { + if(!defaultSettings[name]) { + throw new Error(`Unknown setting ${name}`); + } + + return defaultSettings[name]; +}; + +export const isSettingDefault = (name, value) => { + return getSettingDefault(name) === value; +}; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js index 78233e9305e4f..1e3ea1c8805d2 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js @@ -89,7 +89,7 @@ export const pauseFollowerIndex = (id) => ( handler: async () => ( pauseFollowerIndexRequest(id) ), - onSuccess(response, dispatch, getState) { + onSuccess(response, dispatch) { /** * We can have 1 or more follower index pause operation * that can fail or succeed. We will show 1 toast notification for each. @@ -124,14 +124,6 @@ export const pauseFollowerIndex = (id) => ( toastNotifications.addSuccess(successMessage); - // If we've just paused a follower index we were looking at, we need to close the panel. - // TODO: This is temporary because ES currently removes paused followers from the list. - // Remove once issue is fixed: https://github.com/elastic/elasticsearch/issues/37127 - const followerIndexId = getSelectedFollowerIndexId('detail')(getState()); - if (response.itemsPaused.includes(followerIndexId)) { - dispatch(selectDetailFollowerIndex(null)); - } - // Refresh list dispatch(loadFollowerIndices(true)); } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js index a7a9fea1e8eb5..6256a16316ad3 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js @@ -16,10 +16,8 @@ const initialState = { const success = action => `${action}_SUCCESS`; const parseFollowerIndex = (followerIndex) => { - // Extract remote cluster and leader index from follower index shard information - const { remoteCluster, leaderIndex } = followerIndex.shards[0]; - - return { ...followerIndex, remoteCluster, leaderIndex }; + // Extract status into boolean + return { ...followerIndex, isPaused: followerIndex.status === 'paused' }; }; export const reducer = (state = initialState, action) => { switch (action.type) { diff --git a/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js index 716e4954c69b1..51d176f2cfce7 100644 --- a/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js +++ b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js @@ -63,6 +63,20 @@ export const elasticsearchJsPlugin = (Client, config, components) => { method: 'DELETE' }); + ccr.info = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/info', + req: { + id: { + type: 'string' + } + } + } + ], + method: 'GET' + }); + ccr.stats = ca({ urls: [ { diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/follower_index_serialization.test.js.snap b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/follower_index_serialization.test.js.snap index a41a67089d758..d001459e8234d 100644 --- a/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/follower_index_serialization.test.js.snap +++ b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/follower_index_serialization.test.js.snap @@ -2,7 +2,19 @@ exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = ` Object { - "name": "follower index name", + "leaderIndex": undefined, + "maxOutstandingReadRequests": undefined, + "maxOutstandingWriteRequests": undefined, + "maxReadRequestOperationCount": undefined, + "maxReadRequestSize": undefined, + "maxRetryDelay": undefined, + "maxWriteBufferCount": undefined, + "maxWriteBufferSize": undefined, + "maxWriteRequestOperationCount": undefined, + "maxWriteRequestSize": undefined, + "name": undefined, + "readPollTimeout": undefined, + "remoteCluster": undefined, "shards": Array [ Object { "bytesReadCount": undefined, @@ -61,6 +73,7 @@ Object { "writeBufferSizeBytes": undefined, }, ], + "status": "active", } `; diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.js b/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.js index 6d1da8aa6ff3e..d4605996b471d 100644 --- a/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.js +++ b/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.js @@ -63,10 +63,43 @@ export const deserializeShard = ({ }); /* eslint-enable camelcase */ -export const deserializeFollowerIndex = ({ index, shards }) => ({ - name: index, - shards: shards.map(deserializeShard), +/* eslint-disable camelcase */ +export const deserializeFollowerIndex = ({ + follower_index, + remote_cluster, + leader_index, + status, + parameters: { + max_read_request_operation_count, + max_outstanding_read_requests, + max_read_request_size, + max_write_request_operation_count, + max_write_request_size, + max_outstanding_write_requests, + max_write_buffer_count, + max_write_buffer_size, + max_retry_delay, + read_poll_timeout, + } = {}, + shards, +}) => ({ + name: follower_index, + remoteCluster: remote_cluster, + leaderIndex: leader_index, + status, + maxReadRequestOperationCount: max_read_request_operation_count, + maxOutstandingReadRequests: max_outstanding_read_requests, + maxReadRequestSize: max_read_request_size, + maxWriteRequestOperationCount: max_write_request_operation_count, + maxWriteRequestSize: max_write_request_size, + maxOutstandingWriteRequests: max_outstanding_write_requests, + maxWriteBufferCount: max_write_buffer_count, + maxWriteBufferSize: max_write_buffer_size, + maxRetryDelay: max_retry_delay, + readPollTimeout: read_poll_timeout, + shards: shards && shards.map(deserializeShard), }); +/* eslint-enable camelcase */ export const deserializeListFollowerIndices = followerIndices => followerIndices.map(deserializeFollowerIndex); diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.test.js b/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.test.js index 3f08450884dd2..c6a09d635b711 100644 --- a/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.test.js +++ b/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.test.js @@ -51,6 +51,7 @@ describe('[CCR] follower index serialization', () => { it('deserializes Elasticsearch follower index object', () => { const serializedFollowerIndex = { index: 'follower index name', + status: 'active', shards: [{ shard_id: 'shard 1', }, { @@ -65,18 +66,74 @@ describe('[CCR] follower index serialization', () => { describe('deserializeListFollowerIndices()', () => { it('deserializes list of Elasticsearch follower index objects', () => { const serializedFollowerIndexList = [{ - index: 'follower index 1', + follower_index: 'follower index 1', + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: 1, + max_write_request_operation_count: 1, + max_write_request_size: 1, + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: 1, + max_retry_delay: 1, + read_poll_timeout: 1, + }, shards: [], }, { - index: 'follower index 2', + follower_index: 'follower index 2', + remote_cluster: 'cluster 2', + leader_index: 'leader 2', + status: 'paused', + parameters: { + max_read_request_operation_count: 2, + max_outstanding_read_requests: 2, + max_read_request_size: 2, + max_write_request_operation_count: 2, + max_write_request_size: 2, + max_outstanding_write_requests: 2, + max_write_buffer_count: 2, + max_write_buffer_size: 2, + max_retry_delay: 2, + read_poll_timeout: 2, + }, shards: [], }]; const deserializedFollowerIndexList = [{ name: 'follower index 1', + remoteCluster: 'cluster 1', + leaderIndex: 'leader 1', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: 1, + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: 1, + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: 1, + maxRetryDelay: 1, + readPollTimeout: 1, shards: [], }, { name: 'follower index 2', + remoteCluster: 'cluster 2', + leaderIndex: 'leader 2', + status: 'paused', + maxReadRequestOperationCount: 2, + maxOutstandingReadRequests: 2, + maxReadRequestSize: 2, + maxWriteRequestOperationCount: 2, + maxWriteRequestSize: 2, + maxOutstandingWriteRequests: 2, + maxWriteBufferCount: 2, + maxWriteBufferSize: 2, + maxRetryDelay: 2, + readPollTimeout: 2, shards: [], }]; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js b/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js index 781f4d6ec6cd5..b7f04b29d2f16 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js @@ -7,7 +7,6 @@ import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsErrorFactory } from '../../lib/is_es_error_factory'; import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization'; -import { deserializeListFollowerIndices } from '../../lib/follower_index_serialization'; import { licensePreRoutingFactory } from'../../lib/license_pre_routing_factory'; import { API_BASE_PATH } from '../../../common/constants'; @@ -15,37 +14,6 @@ export const registerCcrRoutes = (server) => { const isEsError = isEsErrorFactory(server); const licensePreRouting = licensePreRoutingFactory(server); - const getStatsHandler = async (request) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await callWithRequest('ccr.stats'); - return { - autoFollow: deserializeAutoFollowStats(response.auto_follow_stats), - follow: { - indices: deserializeListFollowerIndices(response.follow_stats.indices) - } - }; - } catch(err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }; - - /** - * Returns CCR stats - */ - server.route({ - path: `${API_BASE_PATH}/stats`, - method: 'GET', - config: { - pre: [ licensePreRouting ] - }, - handler: getStatsHandler, - }); - /** * Returns Auto-follow stats */ @@ -56,8 +24,20 @@ export const registerCcrRoutes = (server) => { pre: [ licensePreRouting ] }, handler: async (request) => { - const { autoFollow } = await getStatsHandler(request); - return autoFollow; + const callWithRequest = callWithRequestFactory(server, request); + + try { + const { + auto_follow_stats: autoFollowStats, + } = await callWithRequest('ccr.stats'); + + return deserializeAutoFollowStats(autoFollowStats); + } catch(err) { + if (isEsError(err)) { + throw wrapEsError(err); + } + throw wrapUnknownError(err); + } }, }); }; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index.js b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index.js index 6cd70dddbc6ee..c9213eecf8531 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index.js +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index.js @@ -35,9 +35,30 @@ export const registerFollowerIndexRoutes = (server) => { const callWithRequest = callWithRequestFactory(server, request); try { - const response = await callWithRequest('ccr.stats'); + const { + follower_indices: followerIndices + } = await callWithRequest('ccr.info', { id: '_all' }); + + const { + follow_stats: { + indices: followerIndicesStats + } + } = await callWithRequest('ccr.stats'); + + const followerIndicesStatsMap = followerIndicesStats.reduce((map, stats) => { + map[stats.index] = stats; + return map; + }, {}); + + const collatedFollowerIndices = followerIndices.map(followerIndex => { + return { + ...followerIndex, + ...followerIndicesStatsMap[followerIndex.follower_index] + }; + }); + return ({ - indices: deserializeListFollowerIndices(response.follow_stats.indices) + indices: deserializeListFollowerIndices(collatedFollowerIndices) }); } catch(err) { if (isEsError(err)) { @@ -62,15 +83,25 @@ export const registerFollowerIndexRoutes = (server) => { const { id } = request.params; try { - const response = await callWithRequest('ccr.followerIndexStats', { id }); - const followerIndex = response.indices[0]; + const { + follower_indices: followerIndices + } = await callWithRequest('ccr.info', { id }); + + const followerIndexInfo = followerIndices && followerIndices[0]; - if (!followerIndex) { + if(!followerIndexInfo) { const error = Boom.notFound(`The follower index "${id}" does not exist.`); throw(error); } - return deserializeFollowerIndex(followerIndex); + const { + indices: followerIndicesStats + } = await callWithRequest('ccr.followerIndexStats', { id }); + + return deserializeFollowerIndex({ + ...followerIndexInfo, + ...(followerIndicesStats ? followerIndicesStats[0] : {}) + }); } catch(err) { if (isEsError(err)) { throw wrapEsError(err); @@ -117,12 +148,15 @@ export const registerFollowerIndexRoutes = (server) => { handler: async (request) => { const callWithRequest = callWithRequestFactory(server, request); const { id: _id } = request.params; + const { isPaused = false } = request.payload; const body = removeEmptyFields(serializeAdvancedSettings(request.payload)); // We need to first pause the follower and then resume it passing the advanced settings try { - // Pause follower - await callWithRequest('ccr.pauseFollowerIndex', { id: _id }); + // Pause follower if not already paused + if(!isPaused) { + await callWithRequest('ccr.pauseFollowerIndex', { id: _id }); + } // Resume follower return await callWithRequest('ccr.resumeFollowerIndex', { id: _id, body }); @@ -227,8 +261,12 @@ export const registerFollowerIndexRoutes = (server) => { await Promise.all(ids.map(async (_id) => { try { - // Pause follower - await callWithRequest('ccr.pauseFollowerIndex', { id: _id }); + // Try to pause follower, let it fail silently since it may already be paused + try { + await callWithRequest('ccr.pauseFollowerIndex', { id: _id }); + } catch (e) { + // Swallow errors + } // Close index await callWithRequest('indices.close', { index: _id }); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js index f4b34c6c30637..9729c42391787 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js @@ -7,14 +7,22 @@ import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsErrorFactory } from '../../lib/is_es_error_factory'; import { registerFollowerIndexRoutes } from './follower_index'; -import { getFollowerIndexMock, getFollowerIndexListMock } from '../../../fixtures'; +import { + getFollowerIndexStatsMock, + getFollowerIndexListStatsMock, + getFollowerIndexInfoMock, + getFollowerIndexListInfoMock, +} from '../../../fixtures'; import { deserializeFollowerIndex } from '../../lib/follower_index_serialization'; jest.mock('../../lib/call_with_request_factory'); jest.mock('../../lib/is_es_error_factory'); jest.mock('../../lib/license_pre_routing_factory'); -const DESERIALIZED_KEYS = Object.keys(deserializeFollowerIndex(getFollowerIndexMock())); +const DESERIALIZED_KEYS = Object.keys(deserializeFollowerIndex({ + ...getFollowerIndexInfoMock(), + ...getFollowerIndexStatsMock() +})); /** * Hashtable to save the route handlers @@ -99,13 +107,16 @@ describe('[CCR API Routes] Follower Index', () => { it('deserializes the response from Elasticsearch', async () => { const totalResult = 2; - setHttpRequestResponse(null, getFollowerIndexListMock(totalResult)); + const infoResult = getFollowerIndexListInfoMock(totalResult); + const statsResult = getFollowerIndexListStatsMock(totalResult, infoResult.follower_indices.map(index => index.follower_index)); + setHttpRequestResponse(null, infoResult); + setHttpRequestResponse(null, statsResult); const response = await routeHandler(); - const autoFollowPattern = response.indices[0]; + const followerIndex = response.indices[0]; expect(response.indices.length).toEqual(totalResult); - expect(Object.keys(autoFollowPattern)).toEqual(DESERIALIZED_KEYS); + expect(Object.keys(followerIndex)).toEqual(DESERIALIZED_KEYS); }); }); @@ -115,12 +126,14 @@ describe('[CCR API Routes] Follower Index', () => { }); it('should return a single resource even though ES return an array with 1 item', async () => { - const followerIndex = getFollowerIndexMock(); - const esResponse = { indices: [followerIndex] }; + const mockId = 'test1'; + const followerIndexInfo = getFollowerIndexInfoMock(mockId); + const followerIndexStats = getFollowerIndexStatsMock(mockId); - setHttpRequestResponse(null, esResponse); + setHttpRequestResponse(null, { follower_indices: [followerIndexInfo] }); + setHttpRequestResponse(null, { indices: [followerIndexStats] }); - const response = await routeHandler({ params: { id: 1 } }); + const response = await routeHandler({ params: { id: mockId } }); expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS); }); });