Move CCR out of legacy (#62890)

* Convert common/services and server/lib to TypeScript. Update Jest tests.
  - Remove deserializeAutoFollowPattern behavior that returned an empty object if the pattern was undefined.
* Localize mocks with the component integration tests.
* Update API unit tests to use NP mocks.
  - Break up test files.
  - Use inline mocked ES response instead of fixture files.
  - Move remaining fixture files into client integration tests directory.
* Make API route validation more strict.
* Publish isUiDisabled as part of Remote Clusters contract.
* Default trackUiMetric service to be a no-op.
* Remove security dependency.
  - Fix license check so that CCR won't render if the license is invalid.
  - Fix server security check to be more precise by checking if ES has security disabled.
* Render timestamp for autofollow errors.
This commit is contained in:
CJ Cenizal 2020-04-22 07:15:25 -07:00 committed by GitHub
parent aa560353f8
commit 91f7911d15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
251 changed files with 3969 additions and 3231 deletions

View file

@ -96,7 +96,7 @@ module.exports = {
},
},
{
files: ['x-pack/legacy/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'],
files: ['x-pack/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'],
rules: {
'jsx-a11y/click-events-have-key-events': 'off',
},

View file

@ -9,6 +9,7 @@ files:
- 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss'
- 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss'
- 'x-pack/plugins/lens/**/*.s+(a|c)ss'
- 'x-pack/plugins/cross_cluster_replication/**/*.s+(a|c)ss'
- 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss'
- 'x-pack/plugins/maps/**/*.s+(a|c)ss'
ignore:

View file

@ -8,7 +8,7 @@
"xpack.apm": ["legacy/plugins/apm", "plugins/apm"],
"xpack.beatsManagement": "legacy/plugins/beats_management",
"xpack.canvas": "legacy/plugins/canvas",
"xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication",
"xpack.crossClusterReplication": "plugins/cross_cluster_replication",
"xpack.dashboardMode": "legacy/plugins/dashboard_mode",
"xpack.data": "plugins/data_enhanced",
"xpack.drilldowns": "plugins/drilldowns",

View file

@ -21,7 +21,6 @@ import { taskManager } from './legacy/plugins/task_manager';
import { rollup } from './legacy/plugins/rollup';
import { siem } from './legacy/plugins/siem';
import { remoteClusters } from './legacy/plugins/remote_clusters';
import { crossClusterReplication } from './legacy/plugins/cross_cluster_replication';
import { upgradeAssistant } from './legacy/plugins/upgrade_assistant';
import { uptime } from './legacy/plugins/uptime';
import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects';
@ -49,7 +48,6 @@ module.exports = function(kibana) {
rollup(kibana),
siem(kibana),
remoteClusters(kibana),
crossClusterReplication(kibana),
upgradeAssistant(kibana),
uptime(kibana),
encryptedSavedObjects(kibana),

View file

@ -1,11 +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.
*/
export const BASE_PATH = '/management/elasticsearch/cross_cluster_replication';
export const BASE_PATH_REMOTE_CLUSTERS = '/management/elasticsearch/remote_clusters';
export const API_BASE_PATH = '/api/cross_cluster_replication';
export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters';
export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management';

View file

@ -1,10 +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.
*/
export * from './plugin';
export * from './base_path';
export * from './app';
export * from './settings';

View file

@ -1,18 +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.
*/
export const FOLLOWER_INDEX_ADVANCED_SETTINGS = {
maxReadRequestOperationCount: 5120,
maxOutstandingReadRequests: 12,
maxReadRequestSize: '32mb',
maxWriteRequestOperationCount: 5120,
maxWriteRequestSize: '9223372036854775807b',
maxOutstandingWriteRequests: 9,
maxWriteBufferCount: 2147483647,
maxWriteBufferSize: '512mb',
maxRetryDelay: '500ms',
readPollTimeout: '1m',
};

View file

@ -1,128 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = `
Object {
"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,
"failedReadRequestsCount": undefined,
"failedWriteRequestsCount": undefined,
"followerGlobalCheckpoint": undefined,
"followerMappingVersion": undefined,
"followerMaxSequenceNum": undefined,
"followerSettingsVersion": undefined,
"id": "shard 1",
"lastRequestedSequenceNum": undefined,
"leaderGlobalCheckpoint": undefined,
"leaderIndex": undefined,
"leaderMaxSequenceNum": undefined,
"operationsReadCount": undefined,
"operationsWrittenCount": undefined,
"outstandingReadRequestsCount": undefined,
"outstandingWriteRequestsCount": undefined,
"readExceptions": undefined,
"remoteCluster": undefined,
"successfulReadRequestCount": undefined,
"successfulWriteRequestsCount": undefined,
"timeSinceLastReadMs": undefined,
"totalReadRemoteExecTimeMs": undefined,
"totalReadTimeMs": undefined,
"totalWriteTimeMs": undefined,
"writeBufferOperationsCount": undefined,
"writeBufferSizeBytes": undefined,
},
Object {
"bytesReadCount": undefined,
"failedReadRequestsCount": undefined,
"failedWriteRequestsCount": undefined,
"followerGlobalCheckpoint": undefined,
"followerMappingVersion": undefined,
"followerMaxSequenceNum": undefined,
"followerSettingsVersion": undefined,
"id": "shard 2",
"lastRequestedSequenceNum": undefined,
"leaderGlobalCheckpoint": undefined,
"leaderIndex": undefined,
"leaderMaxSequenceNum": undefined,
"operationsReadCount": undefined,
"operationsWrittenCount": undefined,
"outstandingReadRequestsCount": undefined,
"outstandingWriteRequestsCount": undefined,
"readExceptions": undefined,
"remoteCluster": undefined,
"successfulReadRequestCount": undefined,
"successfulWriteRequestsCount": undefined,
"timeSinceLastReadMs": undefined,
"totalReadRemoteExecTimeMs": undefined,
"totalReadTimeMs": undefined,
"totalWriteTimeMs": undefined,
"writeBufferOperationsCount": undefined,
"writeBufferSizeBytes": undefined,
},
],
"status": "active",
}
`;
exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = `
Object {
"bytesReadCount": "bytes read",
"failedReadRequestsCount": "failed read requests",
"failedWriteRequestsCount": "failed write requests",
"followerGlobalCheckpoint": "follower global checkpoint",
"followerMappingVersion": "follower mapping version",
"followerMaxSequenceNum": "follower max seq no",
"followerSettingsVersion": "follower settings version",
"id": "shard id",
"lastRequestedSequenceNum": "last requested seq no",
"leaderGlobalCheckpoint": "leader global checkpoint",
"leaderIndex": "leader index",
"leaderMaxSequenceNum": "leader max seq no",
"operationsReadCount": "operations read",
"operationsWrittenCount": "operations written",
"outstandingReadRequestsCount": "outstanding read requests",
"outstandingWriteRequestsCount": "outstanding write requests",
"readExceptions": Array [
"read exception",
],
"remoteCluster": "remote cluster",
"successfulReadRequestCount": "successful read requests",
"successfulWriteRequestsCount": "successful write requests",
"timeSinceLastReadMs": "time since last read millis",
"totalReadRemoteExecTimeMs": "total read remote exec time millis",
"totalReadTimeMs": "total read time millis",
"totalWriteTimeMs": "total write time millis",
"writeBufferOperationsCount": "write buffer operation count",
"writeBufferSizeBytes": "write buffer size in bytes",
}
`;
exports[`[CCR] follower index serialization serializeFollowerIndex() serializes object to Elasticsearch follower index object 1`] = `
Object {
"leader_index": "leader index",
"max_outstanding_read_requests": "foo",
"max_outstanding_write_requests": "foo",
"max_read_request_operation_count": "foo",
"max_read_request_size": "foo",
"max_retry_delay": "foo",
"max_write_buffer_count": "foo",
"max_write_buffer_size": "foo",
"max_write_request_operation_count": "foo",
"max_write_request_size": "foo",
"read_poll_timeout": "foo",
"remote_cluster": "remote cluster",
}
`;

View file

@ -1,41 +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.
*/
export const deserializeAutoFollowPattern = (
{
name,
pattern: {
active,
// eslint-disable-next-line camelcase
remote_cluster,
// eslint-disable-next-line camelcase
leader_index_patterns,
// eslint-disable-next-line camelcase
follow_index_pattern,
},
} = {
pattern: {},
}
) => ({
name,
active,
remoteCluster: remote_cluster,
leaderIndexPatterns: leader_index_patterns,
followIndexPattern: follow_index_pattern,
});
export const deserializeListAutoFollowPatterns = autoFollowPatterns =>
autoFollowPatterns.map(deserializeAutoFollowPattern);
export const serializeAutoFollowPattern = ({
remoteCluster,
leaderIndexPatterns,
followIndexPattern,
}) => ({
remote_cluster: remoteCluster,
leader_index_patterns: leaderIndexPatterns,
follow_index_pattern: followIndexPattern,
});

View file

@ -1,175 +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 {
deserializeShard,
deserializeFollowerIndex,
deserializeListFollowerIndices,
serializeFollowerIndex,
} from './follower_index_serialization';
describe('[CCR] follower index serialization', () => {
describe('deserializeShard()', () => {
it('deserializes shard', () => {
const serializedShard = {
remote_cluster: 'remote cluster',
leader_index: 'leader index',
shard_id: 'shard id',
leader_global_checkpoint: 'leader global checkpoint',
leader_max_seq_no: 'leader max seq no',
follower_global_checkpoint: 'follower global checkpoint',
follower_max_seq_no: 'follower max seq no',
last_requested_seq_no: 'last requested seq no',
outstanding_read_requests: 'outstanding read requests',
outstanding_write_requests: 'outstanding write requests',
write_buffer_operation_count: 'write buffer operation count',
write_buffer_size_in_bytes: 'write buffer size in bytes',
follower_mapping_version: 'follower mapping version',
follower_settings_version: 'follower settings version',
total_read_time_millis: 'total read time millis',
total_read_remote_exec_time_millis: 'total read remote exec time millis',
successful_read_requests: 'successful read requests',
failed_read_requests: 'failed read requests',
operations_read: 'operations read',
bytes_read: 'bytes read',
total_write_time_millis: 'total write time millis',
successful_write_requests: 'successful write requests',
failed_write_requests: 'failed write requests',
operations_written: 'operations written',
read_exceptions: ['read exception'],
time_since_last_read_millis: 'time since last read millis',
};
expect(deserializeShard(serializedShard)).toMatchSnapshot();
});
});
describe('deserializeFollowerIndex()', () => {
it('deserializes Elasticsearch follower index object', () => {
const serializedFollowerIndex = {
index: 'follower index name',
status: 'active',
shards: [
{
shard_id: 'shard 1',
},
{
shard_id: 'shard 2',
},
],
};
expect(deserializeFollowerIndex(serializedFollowerIndex)).toMatchSnapshot();
});
});
describe('deserializeListFollowerIndices()', () => {
it('deserializes list of Elasticsearch follower index objects', () => {
const serializedFollowerIndexList = [
{
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: [],
},
{
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: [],
},
];
expect(deserializeListFollowerIndices(serializedFollowerIndexList)).toEqual(
deserializedFollowerIndexList
);
});
});
describe('serializeFollowerIndex()', () => {
it('serializes object to Elasticsearch follower index object', () => {
const deserializedFollowerIndex = {
remoteCluster: 'remote cluster',
leaderIndex: 'leader index',
maxReadRequestOperationCount: 'foo',
maxOutstandingReadRequests: 'foo',
maxReadRequestSize: 'foo',
maxWriteRequestOperationCount: 'foo',
maxWriteRequestSize: 'foo',
maxOutstandingWriteRequests: 'foo',
maxWriteBufferCount: 'foo',
maxWriteBufferSize: 'foo',
maxRetryDelay: 'foo',
readPollTimeout: 'foo',
};
expect(serializeFollowerIndex(deserializedFollowerIndex)).toMatchSnapshot();
});
});
});

View file

@ -1,49 +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 { getRandomString } from '../../../../test_utils';
export const getAutoFollowPatternMock = (
name = getRandomString(),
remoteCluster = getRandomString(),
leaderIndexPatterns = [getRandomString()],
followIndexPattern = getRandomString()
) => ({
name,
pattern: {
remote_cluster: remoteCluster,
leader_index_patterns: leaderIndexPatterns,
follow_index_pattern: followIndexPattern,
},
});
export const getAutoFollowPatternListMock = (total = 3) => {
const list = {
patterns: [],
};
let i = total;
while (i--) {
list.patterns.push(getAutoFollowPatternMock());
}
return list;
};
// -----------------
// Client test mock
// -----------------
export const getAutoFollowPatternClientMock = ({
name = getRandomString(),
remoteCluster = getRandomString(),
leaderIndexPatterns = [`${getRandomString()}-*`],
followIndexPattern = getRandomString(),
}) => ({
name,
remoteCluster,
leaderIndexPatterns,
followIndexPattern,
});

View file

@ -1,45 +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.
*/
/**
* Errors mocks to throw during development to help visualizing
* the different flows in the UI
*
* TODO: Consult the ES team and make sure the error shapes are correct
* for each statusCode.
*/
const error400 = new Error('Something went wrong');
error400.statusCode = 400;
error400.response = `
{
"error": {
"root_cause": [
{
"type": "x_content_parse_exception",
"reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found"
}
],
"type": "x_content_parse_exception",
"reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found"
},
"status": 400
}`;
const error403 = new Error('Unauthorized');
error403.statusCode = 403;
error403.response = `
{
"acknowledged": true,
"trial_was_started": false,
"error_message": "Operation failed: Trial was already activated."
}
`;
export const esErrors = {
400: error400,
403: error403,
};

View file

@ -1,216 +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.
*/
const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies
const chance = new Chance();
import { getRandomString } from '../../../../test_utils';
const serializeShard = ({
id,
remoteCluster,
leaderIndex,
leaderGlobalCheckpoint,
leaderMaxSequenceNum,
followerGlobalCheckpoint,
followerMaxSequenceNum,
lastRequestedSequenceNum,
outstandingReadRequestsCount,
outstandingWriteRequestsCount,
writeBufferOperationsCount,
writeBufferSizeBytes,
followerMappingVersion,
followerSettingsVersion,
totalReadTimeMs,
totalReadRemoteExecTimeMs,
successfulReadRequestCount,
failedReadRequestsCount,
operationsReadCount,
bytesReadCount,
totalWriteTimeMs,
successfulWriteRequestsCount,
failedWriteRequestsCount,
operationsWrittenCount,
readExceptions,
timeSinceLastReadMs,
}) => ({
shard_id: id,
remote_cluster: remoteCluster,
leader_index: leaderIndex,
leader_global_checkpoint: leaderGlobalCheckpoint,
leader_max_seq_no: leaderMaxSequenceNum,
follower_global_checkpoint: followerGlobalCheckpoint,
follower_max_seq_no: followerMaxSequenceNum,
last_requested_seq_no: lastRequestedSequenceNum,
outstanding_read_requests: outstandingReadRequestsCount,
outstanding_write_requests: outstandingWriteRequestsCount,
write_buffer_operation_count: writeBufferOperationsCount,
write_buffer_size_in_bytes: writeBufferSizeBytes,
follower_mapping_version: followerMappingVersion,
follower_settings_version: followerSettingsVersion,
total_read_time_millis: totalReadTimeMs,
total_read_remote_exec_time_millis: totalReadRemoteExecTimeMs,
successful_read_requests: successfulReadRequestCount,
failed_read_requests: failedReadRequestsCount,
operations_read: operationsReadCount,
bytes_read: bytesReadCount,
total_write_time_millis: totalWriteTimeMs,
successful_write_requests: successfulWriteRequestsCount,
failed_write_requests: failedWriteRequestsCount,
operations_written: operationsWrittenCount,
read_exceptions: readExceptions,
time_since_last_read_millis: timeSinceLastReadMs,
});
export const getFollowerIndexStatsMock = (
name = chance.string(),
shards = [
{
id: chance.string(),
remoteCluster: chance.string(),
leaderIndex: chance.string(),
leaderGlobalCheckpoint: chance.integer(),
leaderMaxSequenceNum: chance.integer(),
followerGlobalCheckpoint: chance.integer(),
followerMaxSequenceNum: chance.integer(),
lastRequestedSequenceNum: chance.integer(),
outstandingReadRequestsCount: chance.integer(),
outstandingWriteRequestsCount: chance.integer(),
writeBufferOperationsCount: chance.integer(),
writeBufferSizeBytes: chance.integer(),
followerMappingVersion: chance.integer(),
followerSettingsVersion: chance.integer(),
totalReadTimeMs: chance.integer(),
totalReadRemoteExecTimeMs: chance.integer(),
successfulReadRequestCount: chance.integer(),
failedReadRequestsCount: chance.integer(),
operationsReadCount: chance.integer(),
bytesReadCount: chance.integer(),
totalWriteTimeMs: chance.integer(),
successfulWriteRequestsCount: chance.integer(),
failedWriteRequestsCount: chance.integer(),
operationsWrittenCount: chance.integer(),
readExceptions: [chance.string()],
timeSinceLastReadMs: chance.integer(),
},
]
) => ({
index: name,
shards: shards.map(serializeShard),
});
export const getFollowerIndexListStatsMock = (total = 3, names) => {
const list = {
follow_stats: {
indices: [],
},
};
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;
};
// -----------------
// Client test mock
// -----------------
export const getFollowerIndexMock = ({
name = getRandomString(),
remoteCluster = getRandomString(),
leaderIndex = getRandomString(),
status = 'Active',
} = {}) => ({
name,
remoteCluster,
leaderIndex,
status,
maxReadRequestOperationCount: chance.integer(),
maxOutstandingReadRequests: chance.integer(),
maxReadRequestSize: getRandomString({ length: 5 }),
maxWriteRequestOperationCount: chance.integer(),
maxWriteRequestSize: '9223372036854775807b',
maxOutstandingWriteRequests: chance.integer(),
maxWriteBufferCount: chance.integer(),
maxWriteBufferSize: getRandomString({ length: 5 }),
maxRetryDelay: getRandomString({ length: 5 }),
readPollTimeout: getRandomString({ length: 5 }),
shards: [
{
id: 0,
remoteCluster: remoteCluster,
leaderIndex: leaderIndex,
leaderGlobalCheckpoint: chance.integer(),
leaderMaxSequenceNum: chance.integer(),
followerGlobalCheckpoint: chance.integer(),
followerMaxSequenceNum: chance.integer(),
lastRequestedSequenceNum: chance.integer(),
outstandingReadRequestsCount: chance.integer(),
outstandingWriteRequestsCount: chance.integer(),
writeBufferOperationsCount: chance.integer(),
writeBufferSizeBytes: chance.integer(),
followerMappingVersion: chance.integer(),
followerSettingsVersion: chance.integer(),
totalReadTimeMs: chance.integer(),
totalReadRemoteExecTimeMs: chance.integer(),
successfulReadRequestCount: chance.integer(),
failedReadRequestsCount: chance.integer(),
operationsReadCount: chance.integer(),
bytesReadCount: chance.integer(),
totalWriteTimeMs: chance.integer(),
successfulWriteRequestsCount: chance.integer(),
failedWriteRequestsCount: chance.integer(),
operationsWrittenCount: chance.integer(),
readExceptions: [],
timeSinceLastReadMs: chance.integer(),
},
],
});

View file

@ -1,16 +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.
*/
export { getAutoFollowPatternMock, getAutoFollowPatternListMock } from './auto_follow_pattern';
export { esErrors } from './es_errors';
export {
getFollowerIndexStatsMock,
getFollowerIndexListStatsMock,
getFollowerIndexInfoMock,
getFollowerIndexListInfoMock,
} from './follower_index';

View file

@ -1,57 +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 { resolve } from 'path';
import { PLUGIN } from './common/constants';
import { plugin } from './server/np_ready';
export function crossClusterReplication(kibana) {
return new kibana.Plugin({
id: PLUGIN.ID,
configPrefix: 'xpack.ccr',
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'elasticsearch', 'xpack_main', 'remoteClusters', 'index_management'],
uiExports: {
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
managementSections: ['plugins/cross_cluster_replication'],
injectDefaultVars(server) {
const config = server.config();
return {
ccrUiEnabled:
config.get('xpack.ccr.ui.enabled') && config.get('xpack.remote_clusters.ui.enabled'),
};
},
},
config(Joi) {
return Joi.object({
// display menu item
ui: Joi.object({
enabled: Joi.boolean().default(true),
}).default(),
// enable plugin
enabled: Joi.boolean().default(true),
}).default();
},
isEnabled(config) {
return (
config.get('xpack.ccr.enabled') &&
config.get('xpack.index_management.enabled') &&
config.get('xpack.remote_clusters.enabled')
);
},
init: function initCcrPlugin(server) {
plugin({}).setup(server.newPlatform.setup.core, {
indexManagement: server.newPlatform.setup.plugins.indexManagement,
__LEGACY: {
server,
ccrUIEnabled: server.config().get('xpack.ccr.ui.enabled'),
},
});
},
});
}

View file

@ -1,13 +0,0 @@
// Import the EUI global scope so we can use EUI constants
@import 'src/legacy/ui/public/styles/_styling_constants';
// Cross-Cluster Replication plugin styles
// Prefix all styles with "ccr" to avoid conflicts.
// Examples
// ccrChart
// ccrChart__legend
// ccrChart__legend--small
// ccrChart__legend-isLoading
@import 'np_ready/app/app';

View file

@ -1,3 +0,0 @@
<kbn-management-app section="elasticsearch/ccr">
<div id="ccrReactRoot"></div>
</kbn-management-app>

View file

@ -1,14 +0,0 @@
.ccrFollowerIndicesFormRow {
padding-bottom: 0;
}
.ccrFollowerIndicesHelpText {
transform: translateY(-3px);
}
/**
* 1. Prevent context menu popover appearing above confirmation modal
*/
.ccrFollowerIndicesDetailPanel {
z-index: $euiZMask - 1; /* 1 */
}

View file

@ -1,25 +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 React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { HashRouter } from 'react-router-dom';
import { App } from './app';
import { ccrStore } from './store';
export const renderReact = async (elem, I18nContext) => {
render(
<I18nContext>
<Provider store={ccrStore}>
<HashRouter>
<App />
</HashRouter>
</Provider>
</I18nContext>,
elem
);
};

View file

@ -1,22 +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.
*/
let esBase: string;
export const setDocLinks = ({
DOC_LINK_VERSION,
ELASTIC_WEBSITE_URL,
}: {
ELASTIC_WEBSITE_URL: string;
DOC_LINK_VERSION: string;
}) => {
esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`;
};
export const getAutoFollowPatternUrl = () => `${esBase}/ccr-put-auto-follow-pattern.html`;
export const getFollowerIndexUrl = () => `${esBase}/ccr-put-follow.html`;
export const getByteUnitsUrl = () => `${esBase}/common-options.html#byte-units`;
export const getTimeUnitsUrl = () => `${esBase}/common-options.html#time-units`;

View file

@ -1,20 +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 { NotificationsSetup, IToasts, FatalErrorsSetup } from 'src/core/public';
let _notifications: IToasts;
let _fatalErrors: FatalErrorsSetup;
export const setNotifications = (
notifications: NotificationsSetup,
fatalErrorsSetup: FatalErrorsSetup
) => {
_notifications = notifications.toasts;
_fatalErrors = fatalErrorsSetup;
};
export const getNotifications = () => _notifications;
export const getFatalErrors = () => _fatalErrors;

View file

@ -1,27 +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 {
createUiStatsReporter,
METRIC_TYPE,
} from '../../../../../../../../src/legacy/core_plugins/ui_metric/public';
import { UIM_APP_NAME } from '../constants';
export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME);
export { METRIC_TYPE };
/**
* Transparently return provided request Promise, while allowing us to track
* a successful completion of the request.
*/
export function trackUserRequest(request, actionType) {
// Only track successful actions.
return request.then(response => {
trackUiMetric(METRIC_TYPE.LOADED, actionType);
// We return the response immediately without waiting for the tracking request to resolve,
// to avoid adding additional latency.
return response;
});
}

View file

@ -1,28 +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 { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public';
const propertyPath = 'isFollowerIndex';
const followerBadgeExtension = {
matchIndex: (index: any) => {
return get(index, propertyPath);
},
label: i18n.translate('xpack.crossClusterReplication.indexMgmtBadge.followerLabel', {
defaultMessage: 'Follower',
}),
color: 'default',
filterExpression: 'isFollowerIndex:true',
};
export const extendIndexManagement = (indexManagement?: IndexManagementPluginSetup) => {
if (indexManagement) {
indexManagement.extensionsService.addBadge(followerBadgeExtension);
}
};

View file

@ -1,44 +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 {
ChromeBreadcrumb,
CoreSetup,
Plugin,
PluginInitializerContext,
DocLinksStart,
} from 'src/core/public';
import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public';
// @ts-ignore;
import { setHttpClient } from './app/services/api';
import { setBreadcrumbSetter } from './app/services/breadcrumbs';
import { setDocLinks } from './app/services/documentation_links';
import { setNotifications } from './app/services/notifications';
import { extendIndexManagement } from './extend_index_management';
interface PluginDependencies {
indexManagement: IndexManagementPluginSetup;
__LEGACY: {
chrome: any;
MANAGEMENT_BREADCRUMB: ChromeBreadcrumb;
docLinks: DocLinksStart;
};
}
export class CrossClusterReplicationUIPlugin implements Plugin {
// @ts-ignore
constructor(private readonly ctx: PluginInitializerContext) {}
setup({ http, notifications, fatalErrors }: CoreSetup, deps: PluginDependencies) {
setHttpClient(http);
setBreadcrumbSetter(deps);
setDocLinks(deps.__LEGACY.docLinks);
setNotifications(notifications, fatalErrors);
extendIndexManagement(deps.indexManagement);
}
start() {}
}

View file

@ -1,94 +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 { unmountComponentAtNode } from 'react-dom';
import chrome from 'ui/chrome';
import { management, MANAGEMENT_BREADCRUMB } from 'ui/management';
import { npSetup, npStart } from 'ui/new_platform';
import routes from 'ui/routes';
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
import { i18n } from '@kbn/i18n';
import template from './main.html';
import { BASE_PATH } from '../common/constants';
import { plugin } from './np_ready';
/**
* TODO: When this file is deleted, use the management section for rendering
*/
import { renderReact } from './np_ready/app';
const isAvailable = xpackInfo.get('features.crossClusterReplication.isAvailable');
const isActive = xpackInfo.get('features.crossClusterReplication.isActive');
const isLicenseOK = isAvailable && isActive;
const isCcrUiEnabled = chrome.getInjected('ccrUiEnabled');
if (isLicenseOK && isCcrUiEnabled) {
const esSection = management.getSection('elasticsearch');
esSection.register('ccr', {
visible: true,
display: i18n.translate('xpack.crossClusterReplication.appTitle', {
defaultMessage: 'Cross-Cluster Replication',
}),
order: 4,
url: `#${BASE_PATH}`,
});
let elem;
const CCR_REACT_ROOT = 'ccrReactRoot';
plugin({}).setup(npSetup.core, {
...npSetup.plugins,
__LEGACY: {
chrome,
docLinks: npStart.core.docLinks,
MANAGEMENT_BREADCRUMB,
},
});
const unmountReactApp = () => elem && unmountComponentAtNode(elem);
routes.when(`${BASE_PATH}/:section?/:subsection?/:view?/:id?`, {
template,
controllerAs: 'ccr',
controller: class CrossClusterReplicationController {
constructor($scope, $route) {
// React-router's <Redirect> does not play well with the angular router. It will cause this controller
// to re-execute without the $destroy handler being called. This means that the app will be mounted twice
// creating a memory leak when leaving (only 1 app will be unmounted).
// To avoid this, we unmount the React app each time we enter the controller.
unmountReactApp();
$scope.$$postDigest(() => {
elem = document.getElementById(CCR_REACT_ROOT);
renderReact(elem, npStart.core.i18n.Context);
// Angular Lifecycle
const appRoute = $route.current;
const stopListeningForLocationChange = $scope.$on('$locationChangeSuccess', () => {
const currentRoute = $route.current;
const isNavigationInApp = currentRoute.$$route.template === appRoute.$$route.template;
// When we navigate within CCR, prevent Angular from re-matching the route and rebuild the app
if (isNavigationInApp) {
$route.current = appRoute;
} else {
// Any clean up when User leaves the CCR
}
$scope.$on('$destroy', () => {
stopListeningForLocationChange && stopListeningForLocationChange();
unmountReactApp();
});
});
});
}
},
});
}

View file

@ -1,36 +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 { APICaller } from 'src/core/server';
import { Index } from '../../../../../plugins/index_management/server';
export const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => {
if (!indicesList?.length) {
return indicesList;
}
const params = {
path: '/_all/_ccr/info',
method: 'GET',
};
try {
const { follower_indices: followerIndices } = await callWithRequest(
'transport.request',
params
);
return indicesList.map(index => {
const isFollowerIndex = !!followerIndices.find(
(followerIndex: { follower_index: string }) => {
return followerIndex.follower_index === index.name;
}
);
return {
...index,
isFollowerIndex,
};
});
} catch (e) {
return indicesList;
}
};

View file

@ -1,11 +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 { PluginInitializerContext } from 'src/core/server';
import { CrossClusterReplicationServerPlugin } from './plugin';
export const plugin = (ctx: PluginInitializerContext) =>
new CrossClusterReplicationServerPlugin(ctx);

View file

@ -1,20 +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 { once } from 'lodash';
import { elasticsearchJsPlugin } from '../../client/elasticsearch_ccr';
const callWithRequest = once(server => {
const config = { plugins: [elasticsearchJsPlugin] };
const cluster = server.plugins.elasticsearch.createCluster('ccr', config);
return cluster.callWithRequest;
});
export const callWithRequestFactory = (server, request) => {
return (...args) => {
return callWithRequest(server)(request, ...args);
};
};

View file

@ -1,7 +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.
*/
export { callWithRequestFactory } from './call_with_request_factory';

View file

@ -1,70 +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 { i18n } from '@kbn/i18n';
export function checkLicense(xpackLicenseInfo) {
const pluginName = 'Cross-Cluster Replication';
// If, for some reason, we cannot get the license information
// from Elasticsearch, assume worst case and disable
if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) {
return {
isAvailable: false,
showLinks: true,
enableLinks: false,
message: i18n.translate(
'xpack.crossClusterReplication.checkLicense.errorUnavailableMessage',
{
defaultMessage:
'You cannot use {pluginName} because license information is not available at this time.',
values: { pluginName },
}
),
};
}
const VALID_LICENSE_MODES = ['trial', 'platinum', 'enterprise'];
const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES);
const isLicenseActive = xpackLicenseInfo.license.isActive();
const licenseType = xpackLicenseInfo.license.getType();
// License is not valid
if (!isLicenseModeValid) {
return {
isAvailable: false,
isActive: false,
message: i18n.translate(
'xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage',
{
defaultMessage:
'Your {licenseType} license does not support {pluginName}. Please upgrade your license.',
values: { licenseType, pluginName },
}
),
};
}
// License is valid but not active
if (!isLicenseActive) {
return {
isAvailable: true,
isActive: false,
message: i18n.translate('xpack.crossClusterReplication.checkLicense.errorExpiredMessage', {
defaultMessage:
'You cannot use {pluginName} because your {licenseType} license has expired',
values: { licenseType, pluginName },
}),
};
}
// License is valid and active
return {
isAvailable: true,
isActive: true,
};
}

View file

@ -1,33 +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 expect from '@kbn/expect';
import { wrapEsError } from '../wrap_es_error';
describe('wrap_es_error', () => {
describe('#wrapEsError', () => {
let originalError;
beforeEach(() => {
originalError = new Error('I am an error');
originalError.statusCode = 404;
originalError.response = '{}';
});
it('should return the correct object', () => {
const wrappedError = wrapEsError(originalError);
expect(wrappedError.statusCode).to.be(originalError.statusCode);
expect(wrappedError.message).to.be(originalError.message);
});
it('should return the correct object with custom message', () => {
const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' });
expect(wrappedError.statusCode).to.be(originalError.statusCode);
expect(wrappedError.message).to.be('No encontrado!');
});
});
});

View file

@ -1,44 +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 expect from '@kbn/expect';
import { isEsErrorFactory } from '../is_es_error_factory';
import { set } from 'lodash';
class MockAbstractEsError {}
describe('is_es_error_factory', () => {
let mockServer;
let isEsError;
beforeEach(() => {
const mockEsErrors = {
_Abstract: MockAbstractEsError,
};
mockServer = {};
set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors }));
isEsError = isEsErrorFactory(mockServer);
});
describe('#isEsErrorFactory', () => {
it('should return a function', () => {
expect(isEsError).to.be.a(Function);
});
describe('returned function', () => {
it('should return true if passed-in err is a known esError', () => {
const knownEsError = new MockAbstractEsError();
expect(isEsError(knownEsError)).to.be(true);
});
it('should return false if passed-in err is not a known esError', () => {
const unknownEsError = {};
expect(isEsError(unknownEsError)).to.be(false);
});
});
});
});

View file

@ -1,18 +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 { memoize } from 'lodash';
const esErrorsFactory = memoize((server: any) => {
return server.plugins.elasticsearch.getCluster('admin').errors;
});
export function isEsErrorFactory(server: any) {
const esErrors = esErrorsFactory(server);
return function isEsError(err: any) {
return err instanceof esErrors._Abstract;
};
}

View file

@ -1,64 +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 { kibanaResponseFactory } from '../../../../../../../../../src/core/server';
import { licensePreRoutingFactory } from '../license_pre_routing_factory';
describe('license_pre_routing_factory', () => {
describe('#reportingFeaturePreRoutingFactory', () => {
let mockDeps: any;
let mockLicenseCheckResults: any;
const anyContext: any = {};
const anyRequest: any = {};
beforeEach(() => {
mockDeps = {
__LEGACY: {
server: {
plugins: {
xpack_main: {
info: {
feature: () => ({
getLicenseCheckResults: () => mockLicenseCheckResults,
}),
},
},
},
},
},
requestHandler: jest.fn(),
};
});
describe('isAvailable is false', () => {
beforeEach(() => {
mockLicenseCheckResults = {
isAvailable: false,
};
});
it('replies with 403', async () => {
const licensePreRouting = licensePreRoutingFactory(mockDeps);
const response = await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory);
expect(response.status).toBe(403);
});
});
describe('isAvailable is true', () => {
beforeEach(() => {
mockLicenseCheckResults = {
isAvailable: true,
};
});
it('it calls the wrapped handler', async () => {
const licensePreRouting = licensePreRoutingFactory(mockDeps);
await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory);
expect(mockDeps.requestHandler).toHaveBeenCalledTimes(1);
});
});
});
});

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 { RequestHandler } from 'src/core/server';
import { PLUGIN } from '../../../../common/constants';
export const licensePreRoutingFactory = <P, Q, B>({
__LEGACY,
requestHandler,
}: {
__LEGACY: { server: any };
requestHandler: RequestHandler<P, Q, B>;
}) => {
const xpackMainPlugin = __LEGACY.server.plugins.xpack_main;
// License checking and enable/disable logic
const licensePreRouting: RequestHandler<P, Q, B> = (ctx, request, response) => {
const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults();
if (!licenseCheckResults.isAvailable) {
return response.forbidden({
body: licenseCheckResults.message,
});
} else {
return requestHandler(ctx, request, response);
}
};
return licensePreRouting;
};

View file

@ -1,7 +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.
*/
export { registerLicenseChecker } from './register_license_checker';

View file

@ -1,21 +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 { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status';
import { PLUGIN } from '../../../../common/constants';
import { checkLicense } from '../check_license';
export function registerLicenseChecker(__LEGACY) {
const xpackMainPlugin = __LEGACY.server.plugins.xpack_main;
const ccrPluggin = __LEGACY.server.plugins[PLUGIN.ID];
mirrorPluginStatus(xpackMainPlugin, ccrPluggin);
xpackMainPlugin.status.once('green', () => {
// Register a function that is called whenever the xpack info changes,
// to re-compute the license check results for this plugin
xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense);
});
}

View file

@ -1,38 +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 { Plugin, PluginInitializerContext, CoreSetup } from 'src/core/server';
import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/server';
// @ts-ignore
import { registerLicenseChecker } from './lib/register_license_checker';
// @ts-ignore
import { registerRoutes } from './routes/register_routes';
import { ccrDataEnricher } from './cross_cluster_replication_data';
interface PluginDependencies {
indexManagement: IndexManagementPluginSetup;
__LEGACY: {
server: any;
ccrUIEnabled: boolean;
};
}
export class CrossClusterReplicationServerPlugin implements Plugin {
// @ts-ignore
constructor(private readonly ctx: PluginInitializerContext) {}
setup({ http }: CoreSetup, { indexManagement, __LEGACY }: PluginDependencies) {
registerLicenseChecker(__LEGACY);
const router = http.createRouter();
registerRoutes({ router, __LEGACY });
if (__LEGACY.ccrUIEnabled && indexManagement && indexManagement.indexDataEnricher) {
indexManagement.indexDataEnricher.add(ccrDataEnricher);
}
}
start() {}
}

View file

@ -1,330 +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 { deserializeAutoFollowPattern } from '../../../../../common/services/auto_follow_pattern_serialization';
import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../../../fixtures';
import { registerAutoFollowPatternRoutes } from '../auto_follow_pattern';
import { createRouter, callRoute } from './helpers';
jest.mock('../../../lib/call_with_request_factory');
jest.mock('../../../lib/is_es_error_factory');
jest.mock('../../../lib/license_pre_routing_factory', () => ({
licensePreRoutingFactory: ({ requestHandler }) => requestHandler,
}));
const DESERIALIZED_KEYS = Object.keys(deserializeAutoFollowPattern(getAutoFollowPatternMock()));
let routeRegistry;
/**
* Helper to extract all the different server route handler so we can easily call them in our tests.
*
* Important: This method registers the handlers in the order that they appear in the file, so
* if a "server.route()" call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here.
*/
const registerHandlers = () => {
const HANDLER_INDEX_TO_ACTION = {
0: 'list',
1: 'create',
2: 'update',
3: 'get',
4: 'delete',
5: 'pause',
6: 'resume',
};
routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION);
registerAutoFollowPatternRoutes({
__LEGACY: {},
router: routeRegistry.router,
});
};
/**
* Queue to save request response and errors
* It allows us to fake multiple responses from the
* callWithRequestFactory() when the request handler call it
* multiple times.
*/
let requestResponseQueue = [];
/**
* Helper to mock the response from the call to Elasticsearch
*
* @param {*} err The mock error to throw
* @param {*} response The response to return
*/
const setHttpRequestResponse = (error, response) => {
requestResponseQueue.push({ error, response });
};
const resetHttpRequestResponses = () => (requestResponseQueue = []);
const getNextResponseFromQueue = () => {
if (!requestResponseQueue.length) {
return null;
}
const next = requestResponseQueue.shift();
if (next.error) {
return Promise.reject(next.error);
}
return Promise.resolve(next.response);
};
describe('[CCR API Routes] Auto Follow Pattern', () => {
let routeHandler;
beforeAll(() => {
isEsErrorFactory.mockReturnValue(() => false);
callWithRequestFactory.mockReturnValue(getNextResponseFromQueue);
registerHandlers();
});
describe('list()', () => {
beforeEach(() => {
routeHandler = routeRegistry.getRoutes().list;
});
it('should deserialize the response from Elasticsearch', async () => {
const totalResult = 2;
setHttpRequestResponse(null, getAutoFollowPatternListMock(totalResult));
const {
options: { body: response },
} = await callRoute(routeHandler);
const autoFollowPattern = response.patterns[0];
expect(response.patterns.length).toEqual(totalResult);
expect(Object.keys(autoFollowPattern)).toEqual(DESERIALIZED_KEYS);
});
});
describe('create()', () => {
beforeEach(() => {
resetHttpRequestResponses();
routeHandler = routeRegistry.getRoutes().create;
});
it('should throw a 409 conflict error if id already exists', async () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
const response = await callRoute(
routeHandler,
{},
{
body: {
id: 'some-id',
foo: 'bar',
},
}
);
expect(response.status).toEqual(409);
});
it('should return 200 status when the id does not exist', async () => {
const error = new Error('Resource not found.');
error.statusCode = 404;
setHttpRequestResponse(error);
setHttpRequestResponse(null, { acknowledge: true });
const {
options: { body: response },
} = await callRoute(
routeHandler,
{},
{
body: {
id: 'some-id',
foo: 'bar',
},
}
);
expect(response).toEqual({ acknowledge: true });
});
});
describe('update()', () => {
beforeEach(() => {
routeHandler = routeRegistry.getRoutes().update;
});
it('should serialize the payload before sending it to Elasticsearch', async () => {
callWithRequestFactory.mockReturnValueOnce((_, payload) => payload);
const request = {
params: { id: 'foo' },
body: {
remoteCluster: 'bar1',
leaderIndexPatterns: ['bar2'],
followIndexPattern: 'bar3',
},
};
const response = await callRoute(routeHandler, {}, request);
expect(response.options.body).toEqual({
id: 'foo',
body: {
remote_cluster: 'bar1',
leader_index_patterns: ['bar2'],
follow_index_pattern: 'bar3',
},
});
});
});
describe('get()', () => {
beforeEach(() => {
routeHandler = routeRegistry.getRoutes().get;
});
it('should return a single resource even though ES return an array with 1 item', async () => {
const autoFollowPattern = getAutoFollowPatternMock();
const esResponse = { patterns: [autoFollowPattern] };
setHttpRequestResponse(null, esResponse);
const response = await callRoute(routeHandler, {}, { params: { id: 1 } });
expect(Object.keys(response.options.body)).toEqual(DESERIALIZED_KEYS);
});
});
describe('delete()', () => {
beforeEach(() => {
resetHttpRequestResponses();
routeHandler = routeRegistry.getRoutes().delete;
});
it('should delete a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: 'a' } });
expect(response.itemsDeleted).toEqual(['a']);
expect(response.errors).toEqual([]);
});
it('should accept a list of ids to delete', async () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } });
expect(response.options.body.itemsDeleted).toEqual(['a', 'b', 'c']);
});
it('should catch error and return them in array', async () => {
const error = new Error('something went wrong');
error.response = '{ "error": {} }';
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: 'a,b' } });
expect(response.itemsDeleted).toEqual(['a']);
expect(response.errors[0].id).toEqual('b');
});
});
describe('pause()', () => {
beforeEach(() => {
resetHttpRequestResponses();
routeHandler = routeRegistry.getRoutes().pause;
});
it('accept a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: 'a' } });
expect(response.itemsPaused).toEqual(['a']);
expect(response.errors).toEqual([]);
});
it('should accept a list of items to pause', async () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } });
expect(response.options.body.itemsPaused).toEqual(['a', 'b', 'c']);
});
it('should catch error and return them in array', async () => {
const error = new Error('something went wrong');
error.response = '{ "error": {} }';
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: 'a,b' } });
expect(response.itemsPaused).toEqual(['a']);
expect(response.errors[0].id).toEqual('b');
});
});
describe('resume()', () => {
beforeEach(() => {
resetHttpRequestResponses();
routeHandler = routeRegistry.getRoutes().resume;
});
it('accept a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: 'a' } });
expect(response.itemsResumed).toEqual(['a']);
expect(response.errors).toEqual([]);
});
it('should accept a list of items to pause', async () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } });
expect(response.options.body.itemsResumed).toEqual(['a', 'b', 'c']);
});
it('should catch error and return them in array', async () => {
const error = new Error('something went wrong');
error.response = '{ "error": {} }';
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: 'a,b' } });
expect(response.itemsResumed).toEqual(['a']);
expect(response.errors[0].id).toEqual('b');
});
});
});

View file

@ -1,312 +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 { deserializeFollowerIndex } from '../../../../../common/services/follower_index_serialization';
import {
getFollowerIndexStatsMock,
getFollowerIndexListStatsMock,
getFollowerIndexInfoMock,
getFollowerIndexListInfoMock,
} from '../../../../../fixtures';
import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
import { registerFollowerIndexRoutes } from '../follower_index';
import { createRouter, callRoute } from './helpers';
jest.mock('../../../lib/call_with_request_factory');
jest.mock('../../../lib/is_es_error_factory');
jest.mock('../../../lib/license_pre_routing_factory', () => ({
licensePreRoutingFactory: ({ requestHandler }) => requestHandler,
}));
const DESERIALIZED_KEYS = Object.keys(
deserializeFollowerIndex({
...getFollowerIndexInfoMock(),
...getFollowerIndexStatsMock(),
})
);
let routeRegistry;
/**
* Helper to extract all the different server route handler so we can easily call them in our tests.
*
* Important: This method registers the handlers in the order that they appear in the file, so
* if a 'server.route()' call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here.
*/
const registerHandlers = () => {
const HANDLER_INDEX_TO_ACTION = {
0: 'list',
1: 'get',
2: 'create',
3: 'edit',
4: 'pause',
5: 'resume',
6: 'unfollow',
};
routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION);
registerFollowerIndexRoutes({
__LEGACY: {},
router: routeRegistry.router,
});
};
/**
* Queue to save request response and errors
* It allows us to fake multiple responses from the
* callWithRequestFactory() when the request handler call it
* multiple times.
*/
let requestResponseQueue = [];
/**
* Helper to mock the response from the call to Elasticsearch
*
* @param {*} err The mock error to throw
* @param {*} response The response to return
*/
const setHttpRequestResponse = (error, response) => {
requestResponseQueue.push({ error, response });
};
const resetHttpRequestResponses = () => (requestResponseQueue = []);
const getNextResponseFromQueue = () => {
if (!requestResponseQueue.length) {
return null;
}
const next = requestResponseQueue.shift();
if (next.error) {
return Promise.reject(next.error);
}
return Promise.resolve(next.response);
};
describe('[CCR API Routes] Follower Index', () => {
let routeHandler;
beforeAll(() => {
isEsErrorFactory.mockReturnValue(() => false);
callWithRequestFactory.mockReturnValue(getNextResponseFromQueue);
registerHandlers();
});
describe('list()', () => {
beforeEach(() => {
routeHandler = routeRegistry.getRoutes().list;
});
it('deserializes the response from Elasticsearch', async () => {
const totalResult = 2;
const infoResult = getFollowerIndexListInfoMock(totalResult);
const statsResult = getFollowerIndexListStatsMock(
totalResult,
infoResult.follower_indices.map(index => index.follower_index)
);
setHttpRequestResponse(null, infoResult);
setHttpRequestResponse(null, statsResult);
const {
options: { body: response },
} = await callRoute(routeHandler);
const followerIndex = response.indices[0];
expect(response.indices.length).toEqual(totalResult);
expect(Object.keys(followerIndex)).toEqual(DESERIALIZED_KEYS);
});
});
describe('get()', () => {
beforeEach(() => {
routeHandler = routeRegistry.getRoutes().get;
});
it('should return a single resource even though ES return an array with 1 item', async () => {
const mockId = 'test1';
const followerIndexInfo = getFollowerIndexInfoMock(mockId);
const followerIndexStats = getFollowerIndexStatsMock(mockId);
setHttpRequestResponse(null, { follower_indices: [followerIndexInfo] });
setHttpRequestResponse(null, { indices: [followerIndexStats] });
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: mockId } });
expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS);
});
});
describe('create()', () => {
beforeEach(() => {
resetHttpRequestResponses();
routeHandler = routeRegistry.getRoutes().create;
});
it('should return 200 status when follower index is created', async () => {
setHttpRequestResponse(null, { acknowledge: true });
const response = await callRoute(
routeHandler,
{},
{
body: {
name: 'follower_index',
remoteCluster: 'remote_cluster',
leaderIndex: 'leader_index',
},
}
);
expect(response.options.body).toEqual({ acknowledge: true });
});
});
describe('pause()', () => {
beforeEach(() => {
resetHttpRequestResponses();
routeHandler = routeRegistry.getRoutes().pause;
});
it('should pause a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: '1' } });
expect(response.itemsPaused).toEqual(['1']);
expect(response.errors).toEqual([]);
});
it('should accept a list of ids to pause', async () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } });
expect(response.options.body.itemsPaused).toEqual(['1', '2', '3']);
});
it('should catch error and return them in array', async () => {
const error = new Error('something went wrong');
error.response = '{ "error": {} }';
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: '1,2' } });
expect(response.itemsPaused).toEqual(['1']);
expect(response.errors[0].id).toEqual('2');
});
});
describe('resume()', () => {
beforeEach(() => {
resetHttpRequestResponses();
routeHandler = routeRegistry.getRoutes().resume;
});
it('should resume a single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: '1' } });
expect(response.itemsResumed).toEqual(['1']);
expect(response.errors).toEqual([]);
});
it('should accept a list of ids to resume', async () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } });
expect(response.options.body.itemsResumed).toEqual(['1', '2', '3']);
});
it('should catch error and return them in array', async () => {
const error = new Error('something went wrong');
error.response = '{ "error": {} }';
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: '1,2' } });
expect(response.itemsResumed).toEqual(['1']);
expect(response.errors[0].id).toEqual('2');
});
});
describe('unfollow()', () => {
beforeEach(() => {
resetHttpRequestResponses();
routeHandler = routeRegistry.getRoutes().unfollow;
});
it('should unfollow await single item', async () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: '1' } });
expect(response.itemsUnfollowed).toEqual(['1']);
expect(response.errors).toEqual([]);
});
it('should accept a list of ids to unfollow', async () => {
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } });
expect(response.options.body.itemsUnfollowed).toEqual(['1', '2', '3']);
});
it('should catch error and return them in array', async () => {
const error = new Error('something went wrong');
error.response = '{ "error": {} }';
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(null, { acknowledge: true });
setHttpRequestResponse(error);
const {
options: { body: response },
} = await callRoute(routeHandler, {}, { params: { id: '1,2' } });
expect(response.itemsUnfollowed).toEqual(['1']);
expect(response.errors[0].id).toEqual('2');
});
});
});

View file

@ -1,37 +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 { RequestHandler } from 'src/core/server';
import { kibanaResponseFactory } from '../../../../../../../../../src/core/server';
export const callRoute = (
route: RequestHandler<any, any, any>,
ctx = {},
request = {},
response = kibanaResponseFactory
) => {
return route(ctx as any, request as any, response);
};
export const createRouter = (indexToActionMap: Record<number, string>) => {
let index = 0;
const routeHandlers: Record<string, RequestHandler<any, any, any>> = {};
const addHandler = (ignoreCtxForNow: any, handler: RequestHandler) => {
// Save handler and increment index
routeHandlers[indexToActionMap[index]] = handler;
index++;
};
return {
getRoutes: () => routeHandlers,
router: {
get: addHandler,
post: addHandler,
put: addHandler,
delete: addHandler,
},
};
};

View file

@ -1,301 +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 { schema } from '@kbn/config-schema';
// @ts-ignore
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { isEsError } from '../../lib/is_es_error';
// @ts-ignore
import {
deserializeAutoFollowPattern,
deserializeListAutoFollowPatterns,
serializeAutoFollowPattern,
// @ts-ignore
} from '../../../../common/services/auto_follow_pattern_serialization';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { API_BASE_PATH } from '../../../../common/constants';
import { RouteDependencies } from '../types';
import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error';
export const registerAutoFollowPatternRoutes = ({ router, __LEGACY }: RouteDependencies) => {
/**
* Returns a list of all auto-follow patterns
*/
router.get(
{
path: `${API_BASE_PATH}/auto_follow_patterns`,
validate: false,
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
try {
const result = await callWithRequest('ccr.autoFollowPatterns');
return response.ok({
body: {
patterns: deserializeListAutoFollowPatterns(result.patterns),
},
});
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
/**
* Create an auto-follow pattern
*/
router.post(
{
path: `${API_BASE_PATH}/auto_follow_patterns`,
validate: {
body: schema.object(
{
id: schema.string(),
},
{ unknowns: 'allow' }
),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id, ...rest } = request.body;
const body = serializeAutoFollowPattern(rest);
/**
* First let's make sur that an auto-follow pattern with
* the same id does not exist.
*/
try {
await callWithRequest('ccr.autoFollowPattern', { id });
// If we get here it means that an auto-follow pattern with the same id exists
return response.conflict({
body: `An auto-follow pattern with the name "${id}" already exists.`,
});
} catch (err) {
if (err.statusCode !== 404) {
return mapErrorToKibanaHttpResponse(err);
}
}
try {
return response.ok({
body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }),
});
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
/**
* Update an auto-follow pattern
*/
router.put(
{
path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
body: schema.object({}, { unknowns: 'allow' }),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
const body = serializeAutoFollowPattern(request.body);
try {
return response.ok({
body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }),
});
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
/**
* Returns a single auto-follow pattern
*/
router.get(
{
path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
try {
const result = await callWithRequest('ccr.autoFollowPattern', { id });
const autoFollowPattern = result.patterns[0];
return response.ok({
body: deserializeAutoFollowPattern(autoFollowPattern),
});
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
/**
* Delete an auto-follow pattern
*/
router.delete(
{
path: `${API_BASE_PATH}/auto_follow_patterns/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
const ids = id.split(',');
const itemsDeleted: string[] = [];
const errors: Array<{ id: string; error: any }> = [];
await Promise.all(
ids.map(_id =>
callWithRequest('ccr.deleteAutoFollowPattern', { id: _id })
.then(() => itemsDeleted.push(_id))
.catch((err: Error) => {
if (isEsError(err)) {
errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
} else {
errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
}
})
)
);
return response.ok({
body: {
itemsDeleted,
errors,
},
});
},
})
);
/**
* Pause auto-follow pattern(s)
*/
router.post(
{
path: `${API_BASE_PATH}/auto_follow_patterns/{id}/pause`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
const ids = id.split(',');
const itemsPaused: string[] = [];
const errors: Array<{ id: string; error: any }> = [];
await Promise.all(
ids.map(_id =>
callWithRequest('ccr.pauseAutoFollowPattern', { id: _id })
.then(() => itemsPaused.push(_id))
.catch((err: Error) => {
if (isEsError(err)) {
errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
} else {
errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
}
})
)
);
return response.ok({
body: {
itemsPaused,
errors,
},
});
},
})
);
/**
* Resume auto-follow pattern(s)
*/
router.post(
{
path: `${API_BASE_PATH}/auto_follow_patterns/{id}/resume`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
const ids = id.split(',');
const itemsResumed: string[] = [];
const errors: Array<{ id: string; error: any }> = [];
await Promise.all(
ids.map(_id =>
callWithRequest('ccr.resumeAutoFollowPattern', { id: _id })
.then(() => itemsResumed.push(_id))
.catch((err: Error) => {
if (isEsError(err)) {
errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
} else {
errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
}
})
)
);
return response.ok({
body: {
itemsResumed,
errors,
},
});
},
})
);
};

View file

@ -1,112 +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 { API_BASE_PATH } from '../../../../common/constants';
// @ts-ignore
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
// @ts-ignore
import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error';
import { RouteDependencies } from '../types';
export const registerCcrRoutes = ({ router, __LEGACY }: RouteDependencies) => {
/**
* Returns Auto-follow stats
*/
router.get(
{
path: `${API_BASE_PATH}/stats/auto_follow`,
validate: false,
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
try {
const { auto_follow_stats: autoFollowStats } = await callWithRequest('ccr.stats');
return response.ok({
body: deserializeAutoFollowStats(autoFollowStats),
});
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
/**
* Returns whether the user has CCR permissions
*/
router.get(
{
path: `${API_BASE_PATH}/permissions`,
validate: false,
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const xpackMainPlugin = __LEGACY.server.plugins.xpack_main;
const xpackInfo = xpackMainPlugin && xpackMainPlugin.info;
if (!xpackInfo) {
// xpackInfo is updated via poll, so it may not be available until polling has begun.
// In this rare situation, tell the client the service is temporarily unavailable.
return response.customError({
statusCode: 503,
body: 'Security info unavailable',
});
}
const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security');
if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) {
// If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR.
return response.ok({
body: {
hasPermission: true,
missingClusterPrivileges: [],
},
});
}
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
try {
const { has_all_requested: hasPermission, cluster } = await callWithRequest(
'ccr.permissions',
{
body: {
cluster: ['manage', 'manage_ccr'],
},
}
);
const missingClusterPrivileges = Object.keys(cluster).reduce(
(permissions: any, permissionName: any) => {
if (!cluster[permissionName]) {
permissions.push(permissionName);
return permissions;
}
},
[] as any[]
);
return response.ok({
body: {
hasPermission,
missingClusterPrivileges,
},
});
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
};

View file

@ -1,357 +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 { schema } from '@kbn/config-schema';
import {
deserializeFollowerIndex,
deserializeListFollowerIndices,
serializeFollowerIndex,
serializeAdvancedSettings,
// @ts-ignore
} from '../../../../common/services/follower_index_serialization';
import { API_BASE_PATH } from '../../../../common/constants';
// @ts-ignore
import { removeEmptyFields } from '../../../../common/services/utils';
// @ts-ignore
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { RouteDependencies } from '../types';
import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error';
export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependencies) => {
/**
* Returns a list of all follower indices
*/
router.get(
{
path: `${API_BASE_PATH}/follower_indices`,
validate: false,
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
try {
const { follower_indices: followerIndices } = await callWithRequest('ccr.info', {
id: '_all',
});
const {
follow_stats: { indices: followerIndicesStats },
} = await callWithRequest('ccr.stats');
const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => {
map[stats.index] = stats;
return map;
}, {});
const collatedFollowerIndices = followerIndices.map((followerIndex: any) => {
return {
...followerIndex,
...followerIndicesStatsMap[followerIndex.follower_index],
};
});
return response.ok({
body: {
indices: deserializeListFollowerIndices(collatedFollowerIndices),
},
});
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
/**
* Returns a single follower index pattern
*/
router.get(
{
path: `${API_BASE_PATH}/follower_indices/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
try {
const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id });
const followerIndexInfo = followerIndices && followerIndices[0];
if (!followerIndexInfo) {
return response.notFound({
body: `The follower index "${id}" does not exist.`,
});
}
// If this follower is paused, skip call to ES stats api since it will return 404
if (followerIndexInfo.status === 'paused') {
return response.ok({
body: deserializeFollowerIndex({
...followerIndexInfo,
}),
});
} else {
const {
indices: followerIndicesStats,
} = await callWithRequest('ccr.followerIndexStats', { id });
return response.ok({
body: deserializeFollowerIndex({
...followerIndexInfo,
...(followerIndicesStats ? followerIndicesStats[0] : {}),
}),
});
}
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
/**
* Create a follower index
*/
router.post(
{
path: `${API_BASE_PATH}/follower_indices`,
validate: {
body: schema.object(
{
name: schema.string(),
},
{ unknowns: 'allow' }
),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { name, ...rest } = request.body;
const body = removeEmptyFields(serializeFollowerIndex(rest));
try {
return response.ok({
body: await callWithRequest('ccr.saveFollowerIndex', { name, body }),
});
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
/**
* Edit a follower index
*/
router.put(
{
path: `${API_BASE_PATH}/follower_indices/{id}`,
validate: {
params: schema.object({ id: schema.string() }),
body: schema.object({
maxReadRequestOperationCount: schema.maybe(schema.number()),
maxOutstandingReadRequests: schema.maybe(schema.number()),
maxReadRequestSize: schema.maybe(schema.string()), // byte value
maxWriteRequestOperationCount: schema.maybe(schema.number()),
maxWriteRequestSize: schema.maybe(schema.string()), // byte value
maxOutstandingWriteRequests: schema.maybe(schema.number()),
maxWriteBufferCount: schema.maybe(schema.number()),
maxWriteBufferSize: schema.maybe(schema.string()), // byte value
maxRetryDelay: schema.maybe(schema.string()), // time value
readPollTimeout: schema.maybe(schema.string()), // time value
}),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
// We need to first pause the follower and then resume it passing the advanced settings
try {
const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id });
const followerIndexInfo = followerIndices && followerIndices[0];
if (!followerIndexInfo) {
return response.notFound({ body: `The follower index "${id}" does not exist.` });
}
// Retrieve paused state instead of pulling it from the payload to ensure it's not stale.
const isPaused = followerIndexInfo.status === 'paused';
// Pause follower if not already paused
if (!isPaused) {
await callWithRequest('ccr.pauseFollowerIndex', { id });
}
// Resume follower
const body = removeEmptyFields(serializeAdvancedSettings(request.body));
return response.ok({
body: await callWithRequest('ccr.resumeFollowerIndex', { id, body }),
});
} catch (err) {
return mapErrorToKibanaHttpResponse(err);
}
},
})
);
/**
* Pauses a follower index
*/
router.put(
{
path: `${API_BASE_PATH}/follower_indices/{id}/pause`,
validate: {
params: schema.object({ id: schema.string() }),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
const ids = id.split(',');
const itemsPaused: string[] = [];
const errors: Array<{ id: string; error: any }> = [];
await Promise.all(
ids.map(_id =>
callWithRequest('ccr.pauseFollowerIndex', { id: _id })
.then(() => itemsPaused.push(_id))
.catch((err: Error) => {
errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
})
)
);
return response.ok({
body: {
itemsPaused,
errors,
},
});
},
})
);
/**
* Resumes a follower index
*/
router.put(
{
path: `${API_BASE_PATH}/follower_indices/{id}/resume`,
validate: {
params: schema.object({ id: schema.string() }),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
const ids = id.split(',');
const itemsResumed: string[] = [];
const errors: Array<{ id: string; error: any }> = [];
await Promise.all(
ids.map(_id =>
callWithRequest('ccr.resumeFollowerIndex', { id: _id })
.then(() => itemsResumed.push(_id))
.catch((err: Error) => {
errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
})
)
);
return response.ok({
body: {
itemsResumed,
errors,
},
});
},
})
);
/**
* Unfollow follower index's leader index
*/
router.put(
{
path: `${API_BASE_PATH}/follower_indices/{id}/unfollow`,
validate: {
params: schema.object({ id: schema.string() }),
},
},
licensePreRoutingFactory({
__LEGACY,
requestHandler: async (ctx, request, response) => {
const callWithRequest = callWithRequestFactory(__LEGACY.server, request);
const { id } = request.params;
const ids = id.split(',');
const itemsUnfollowed: string[] = [];
const itemsNotOpen: string[] = [];
const errors: Array<{ id: string; error: any }> = [];
await Promise.all(
ids.map(async _id => {
try {
// 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 });
// Unfollow leader
await callWithRequest('ccr.unfollowLeaderIndex', { id: _id });
// Try to re-open the index, store failures in a separate array to surface warnings in the UI
// This will allow users to query their index normally after unfollowing
try {
await callWithRequest('indices.open', { index: _id });
} catch (e) {
itemsNotOpen.push(_id);
}
// Push success
itemsUnfollowed.push(_id);
} catch (err) {
errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) });
}
})
);
return response.ok({
body: {
itemsUnfollowed,
itemsNotOpen,
errors,
},
});
},
})
);
};

View file

@ -1,26 +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 { kibanaResponseFactory } from '../../../../../../../src/core/server';
// @ts-ignore
import { wrapEsError } from '../lib/error_wrappers';
import { isEsError } from '../lib/is_es_error';
export const mapErrorToKibanaHttpResponse = (err: any) => {
if (isEsError(err)) {
const { statusCode, message, body } = wrapEsError(err);
return kibanaResponseFactory.customError({
statusCode,
body: {
message,
attributes: {
cause: body?.cause,
},
},
});
}
return kibanaResponseFactory.internalError(err);
};

View file

@ -1,13 +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 { IRouter } from 'src/core/server';
export interface RouteDependencies {
router: IRouter;
__LEGACY: {
server: any;
};
}

View file

@ -0,0 +1,44 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { LicenseType } from '../../../licensing/common/types';
const platinumLicense: LicenseType = 'platinum';
export const PLUGIN = {
ID: 'crossClusterReplication',
TITLE: i18n.translate('xpack.crossClusterReplication.appTitle', {
defaultMessage: 'Cross-Cluster Replication',
}),
minimumLicenseType: platinumLicense,
};
export const APPS = {
CCR_APP: 'ccr',
REMOTE_CLUSTER_APP: 'remote_cluster',
};
export const MANAGEMENT_ID = 'cross_cluster_replication';
export const BASE_PATH = `/management/elasticsearch/${MANAGEMENT_ID}`;
export const BASE_PATH_REMOTE_CLUSTERS = '/management/elasticsearch/remote_clusters';
export const API_BASE_PATH = '/api/cross_cluster_replication';
export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters';
export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management';
export const FOLLOWER_INDEX_ADVANCED_SETTINGS = {
maxReadRequestOperationCount: 5120,
maxOutstandingReadRequests: 12,
maxReadRequestSize: '32mb',
maxWriteRequestOperationCount: 5120,
maxWriteRequestSize: '9223372036854775807b',
maxOutstandingWriteRequests: 9,
maxWriteBufferCount: 2147483647,
maxWriteBufferSize: '512mb',
maxRetryDelay: '500ms',
readPollTimeout: '1m',
};

View file

@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = `
Object {
"leaderIndex": "leader 1",
"maxOutstandingReadRequests": 1,
"maxOutstandingWriteRequests": 1,
"maxReadRequestOperationCount": 1,
"maxReadRequestSize": "1b",
"maxRetryDelay": "1s",
"maxWriteBufferCount": 1,
"maxWriteBufferSize": "1b",
"maxWriteRequestOperationCount": 1,
"maxWriteRequestSize": "1b",
"name": "follower index 1",
"readPollTimeout": "1s",
"remoteCluster": "cluster 1",
"shards": Array [
Object {
"bytesReadCount": 1,
"failedReadRequestsCount": 1,
"failedWriteRequestsCount": 1,
"followerGlobalCheckpoint": 1,
"followerMappingVersion": 1,
"followerMaxSequenceNum": 1,
"followerSettingsVersion": 1,
"id": 1,
"lastRequestedSequenceNum": 1,
"leaderGlobalCheckpoint": 1,
"leaderIndex": "leader 1",
"leaderMaxSequenceNum": 1,
"operationsReadCount": 1,
"operationsWrittenCount": 1,
"outstandingReadRequestsCount": 1,
"outstandingWriteRequestsCount": 1,
"readExceptions": Array [],
"remoteCluster": "cluster 1",
"successfulReadRequestCount": 1,
"successfulWriteRequestsCount": 1,
"timeSinceLastReadMs": 1,
"totalReadRemoteExecTimeMs": 1,
"totalReadTimeMs": 1,
"totalWriteTimeMs": 1,
"writeBufferOperationsCount": 1,
"writeBufferSizeBytes": 1,
},
Object {
"bytesReadCount": undefined,
"failedReadRequestsCount": undefined,
"failedWriteRequestsCount": undefined,
"followerGlobalCheckpoint": undefined,
"followerMappingVersion": undefined,
"followerMaxSequenceNum": undefined,
"followerSettingsVersion": undefined,
"id": "shard 2",
"lastRequestedSequenceNum": undefined,
"leaderGlobalCheckpoint": undefined,
"leaderIndex": "leader_index 2",
"leaderMaxSequenceNum": undefined,
"operationsReadCount": undefined,
"operationsWrittenCount": undefined,
"outstandingReadRequestsCount": undefined,
"outstandingWriteRequestsCount": undefined,
"readExceptions": undefined,
"remoteCluster": "remote_cluster 2",
"successfulReadRequestCount": undefined,
"successfulWriteRequestsCount": undefined,
"timeSinceLastReadMs": undefined,
"totalReadRemoteExecTimeMs": undefined,
"totalReadTimeMs": undefined,
"totalWriteTimeMs": undefined,
"writeBufferOperationsCount": undefined,
"writeBufferSizeBytes": undefined,
},
],
"status": "active",
}
`;
exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = `
Object {
"bytesReadCount": 1,
"failedReadRequestsCount": 1,
"failedWriteRequestsCount": 1,
"followerGlobalCheckpoint": 1,
"followerMappingVersion": 1,
"followerMaxSequenceNum": 1,
"followerSettingsVersion": 1,
"id": 1,
"lastRequestedSequenceNum": 1,
"leaderGlobalCheckpoint": 1,
"leaderIndex": "leader index",
"leaderMaxSequenceNum": 1,
"operationsReadCount": 1,
"operationsWrittenCount": 1,
"outstandingReadRequestsCount": 1,
"outstandingWriteRequestsCount": 1,
"readExceptions": Array [
"read exception",
],
"remoteCluster": "remote cluster",
"successfulReadRequestCount": 1,
"successfulWriteRequestsCount": 1,
"timeSinceLastReadMs": 1,
"totalReadRemoteExecTimeMs": 1,
"totalReadTimeMs": 1,
"totalWriteTimeMs": 1,
"writeBufferOperationsCount": 1,
"writeBufferSizeBytes": 1,
}
`;
exports[`[CCR] follower index serialization serializeFollowerIndex() serializes object to Elasticsearch follower index object 1`] = `
Object {
"leader_index": "leader index",
"max_outstanding_read_requests": 1,
"max_outstanding_write_requests": 1,
"max_read_request_operation_count": 1,
"max_read_request_size": "1b",
"max_retry_delay": "1s",
"max_write_buffer_count": 1,
"max_write_buffer_size": "1b",
"max_write_request_operation_count": 1,
"max_write_request_size": "1b",
"read_poll_timeout": "1s",
"remote_cluster": "remote cluster",
}
`;

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AutoFollowPattern, AutoFollowPatternFromEs } from '../types';
import {
deserializeAutoFollowPattern,
deserializeListAutoFollowPatterns,
@ -12,13 +14,10 @@ import {
describe('[CCR] auto-follow_serialization', () => {
describe('deserializeAutoFollowPattern()', () => {
it('should return empty object if name or esObject are not provided', () => {
expect(deserializeAutoFollowPattern()).toEqual({});
});
it('should deserialize Elasticsearch object', () => {
const expected = {
name: 'some-name',
active: true,
remoteCluster: 'foo',
leaderIndexPatterns: ['foo-*'],
followIndexPattern: 'bar',
@ -27,13 +26,14 @@ describe('[CCR] auto-follow_serialization', () => {
const esObject = {
name: 'some-name',
pattern: {
active: true,
remote_cluster: expected.remoteCluster,
leader_index_patterns: expected.leaderIndexPatterns,
follow_index_pattern: expected.followIndexPattern,
},
};
expect(deserializeAutoFollowPattern(esObject)).toEqual(expected);
expect(deserializeAutoFollowPattern(esObject as AutoFollowPatternFromEs)).toEqual(expected);
});
});
@ -78,7 +78,9 @@ describe('[CCR] auto-follow_serialization', () => {
],
};
expect(deserializeListAutoFollowPatterns(esObjects.patterns)).toEqual(expected);
expect(
deserializeListAutoFollowPatterns(esObjects.patterns as AutoFollowPatternFromEs[])
).toEqual(expected);
});
});
@ -96,7 +98,7 @@ describe('[CCR] auto-follow_serialization', () => {
followIndexPattern: expected.follow_index_pattern,
};
expect(serializeAutoFollowPattern(object)).toEqual(expected);
expect(serializeAutoFollowPattern(object as AutoFollowPattern)).toEqual(expected);
});
});
});

View file

@ -0,0 +1,38 @@
/*
* 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 { AutoFollowPattern, AutoFollowPatternFromEs, AutoFollowPatternToEs } from '../types';
export const deserializeAutoFollowPattern = (
autoFollowPattern: AutoFollowPatternFromEs
): AutoFollowPattern => {
const {
name,
pattern: { active, remote_cluster, leader_index_patterns, follow_index_pattern },
} = autoFollowPattern;
return {
name,
active,
remoteCluster: remote_cluster,
leaderIndexPatterns: leader_index_patterns,
followIndexPattern: follow_index_pattern,
};
};
export const deserializeListAutoFollowPatterns = (
autoFollowPatterns: AutoFollowPatternFromEs[]
): AutoFollowPattern[] => autoFollowPatterns.map(deserializeAutoFollowPattern);
export const serializeAutoFollowPattern = ({
remoteCluster,
leaderIndexPatterns,
followIndexPattern,
}: AutoFollowPattern): AutoFollowPatternToEs => ({
remote_cluster: remoteCluster,
leader_index_patterns: leaderIndexPatterns,
follow_index_pattern: followIndexPattern,
});

View file

@ -0,0 +1,224 @@
/*
* 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 { ShardFromEs, FollowerIndexFromEs, FollowerIndex } from '../types';
import {
deserializeShard,
deserializeFollowerIndex,
deserializeListFollowerIndices,
serializeFollowerIndex,
} from './follower_index_serialization';
describe('[CCR] follower index serialization', () => {
describe('deserializeShard()', () => {
it('deserializes shard', () => {
const serializedShard = {
remote_cluster: 'remote cluster',
leader_index: 'leader index',
shard_id: 1,
leader_global_checkpoint: 1,
leader_max_seq_no: 1,
follower_global_checkpoint: 1,
follower_max_seq_no: 1,
last_requested_seq_no: 1,
outstanding_read_requests: 1,
outstanding_write_requests: 1,
write_buffer_operation_count: 1,
write_buffer_size_in_bytes: 1,
follower_mapping_version: 1,
follower_settings_version: 1,
total_read_time_millis: 1,
total_read_remote_exec_time_millis: 1,
successful_read_requests: 1,
failed_read_requests: 1,
operations_read: 1,
bytes_read: 1,
total_write_time_millis: 1,
successful_write_requests: 1,
failed_write_requests: 1,
operations_written: 1,
read_exceptions: ['read exception'],
time_since_last_read_millis: 1,
};
expect(deserializeShard(serializedShard as ShardFromEs)).toMatchSnapshot();
});
});
describe('deserializeFollowerIndex()', () => {
it('deserializes Elasticsearch follower index object', () => {
const serializedFollowerIndex = {
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: '1b',
max_write_request_operation_count: 1,
max_write_request_size: '1b',
max_outstanding_write_requests: 1,
max_write_buffer_count: 1,
max_write_buffer_size: '1b',
max_retry_delay: '1s',
read_poll_timeout: '1s',
},
shards: [
{
remote_cluster: 'cluster 1',
leader_index: 'leader 1',
shard_id: 1,
leader_global_checkpoint: 1,
leader_max_seq_no: 1,
follower_global_checkpoint: 1,
follower_max_seq_no: 1,
last_requested_seq_no: 1,
outstanding_read_requests: 1,
outstanding_write_requests: 1,
write_buffer_operation_count: 1,
write_buffer_size_in_bytes: 1,
follower_mapping_version: 1,
follower_settings_version: 1,
total_read_time_millis: 1,
total_read_remote_exec_time_millis: 1,
successful_read_requests: 1,
failed_read_requests: 1,
operations_read: 1,
bytes_read: 1,
total_write_time_millis: 1,
successful_write_requests: 1,
failed_write_requests: 1,
operations_written: 1,
// This is an array of exception objects
read_exceptions: [],
time_since_last_read_millis: 1,
},
{
remote_cluster: 'remote_cluster 2',
leader_index: 'leader_index 2',
shard_id: 'shard 2',
},
],
};
expect(
deserializeFollowerIndex(serializedFollowerIndex as FollowerIndexFromEs)
).toMatchSnapshot();
});
});
describe('deserializeListFollowerIndices()', () => {
it('deserializes list of Elasticsearch follower index objects', () => {
const serializedFollowerIndexList = [
{
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: '1b',
max_write_request_operation_count: 1,
max_write_request_size: '1b',
max_outstanding_write_requests: 1,
max_write_buffer_count: 1,
max_write_buffer_size: '1b',
max_retry_delay: '1s',
read_poll_timeout: '1s',
},
shards: [],
},
{
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: '2b',
max_write_request_operation_count: 2,
max_write_request_size: '2b',
max_outstanding_write_requests: 2,
max_write_buffer_count: 2,
max_write_buffer_size: '2b',
max_retry_delay: '2s',
read_poll_timeout: '2s',
},
shards: [],
},
];
const deserializedFollowerIndexList = [
{
name: 'follower index 1',
remoteCluster: 'cluster 1',
leaderIndex: 'leader 1',
status: 'active',
maxReadRequestOperationCount: 1,
maxOutstandingReadRequests: 1,
maxReadRequestSize: '1b',
maxWriteRequestOperationCount: 1,
maxWriteRequestSize: '1b',
maxOutstandingWriteRequests: 1,
maxWriteBufferCount: 1,
maxWriteBufferSize: '1b',
maxRetryDelay: '1s',
readPollTimeout: '1s',
shards: [],
},
{
name: 'follower index 2',
remoteCluster: 'cluster 2',
leaderIndex: 'leader 2',
status: 'paused',
maxReadRequestOperationCount: 2,
maxOutstandingReadRequests: 2,
maxReadRequestSize: '2b',
maxWriteRequestOperationCount: 2,
maxWriteRequestSize: '2b',
maxOutstandingWriteRequests: 2,
maxWriteBufferCount: 2,
maxWriteBufferSize: '2b',
maxRetryDelay: '2s',
readPollTimeout: '2s',
shards: [],
},
];
expect(deserializeListFollowerIndices(serializedFollowerIndexList)).toEqual(
deserializedFollowerIndexList
);
});
});
describe('serializeFollowerIndex()', () => {
it('serializes object to Elasticsearch follower index object', () => {
const deserializedFollowerIndex = {
name: 'test',
status: 'active',
shards: [],
remoteCluster: 'remote cluster',
leaderIndex: 'leader index',
maxReadRequestOperationCount: 1,
maxOutstandingReadRequests: 1,
maxReadRequestSize: '1b',
maxWriteRequestOperationCount: 1,
maxWriteRequestSize: '1b',
maxOutstandingWriteRequests: 1,
maxWriteBufferCount: 1,
maxWriteBufferSize: '1b',
maxRetryDelay: '1s',
readPollTimeout: '1s',
};
expect(serializeFollowerIndex(deserializedFollowerIndex as FollowerIndex)).toMatchSnapshot();
});
});
});

View file

@ -4,7 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable camelcase */
import {
ShardFromEs,
Shard,
FollowerIndexFromEs,
FollowerIndex,
FollowerIndexToEs,
FollowerIndexAdvancedSettings,
FollowerIndexAdvancedSettingsToEs,
} from '../types';
export const deserializeShard = ({
remote_cluster,
leader_index,
@ -32,7 +41,7 @@ export const deserializeShard = ({
operations_written,
read_exceptions,
time_since_last_read_millis,
}) => ({
}: ShardFromEs): Shard => ({
id: shard_id,
remoteCluster: remote_cluster,
leaderIndex: leader_index,
@ -61,9 +70,7 @@ export const deserializeShard = ({
readExceptions: read_exceptions,
timeSinceLastReadMs: time_since_last_read_millis,
});
/* eslint-enable camelcase */
/* eslint-disable camelcase */
export const deserializeFollowerIndex = ({
follower_index,
remote_cluster,
@ -82,7 +89,7 @@ export const deserializeFollowerIndex = ({
read_poll_timeout,
} = {},
shards,
}) => ({
}: FollowerIndexFromEs): FollowerIndex => ({
name: follower_index,
remoteCluster: remote_cluster,
leaderIndex: leader_index,
@ -99,10 +106,10 @@ export const deserializeFollowerIndex = ({
readPollTimeout: read_poll_timeout,
shards: shards && shards.map(deserializeShard),
});
/* eslint-enable camelcase */
export const deserializeListFollowerIndices = followerIndices =>
followerIndices.map(deserializeFollowerIndex);
export const deserializeListFollowerIndices = (
followerIndices: FollowerIndexFromEs[]
): FollowerIndex[] => followerIndices.map(deserializeFollowerIndex);
export const serializeAdvancedSettings = ({
maxReadRequestOperationCount,
@ -115,7 +122,7 @@ export const serializeAdvancedSettings = ({
maxWriteBufferSize,
maxRetryDelay,
readPollTimeout,
}) => ({
}: FollowerIndexAdvancedSettings): FollowerIndexAdvancedSettingsToEs => ({
max_read_request_operation_count: maxReadRequestOperationCount,
max_outstanding_read_requests: maxOutstandingReadRequests,
max_read_request_size: maxReadRequestSize,
@ -128,7 +135,7 @@ export const serializeAdvancedSettings = ({
read_poll_timeout: readPollTimeout,
});
export const serializeFollowerIndex = followerIndex => ({
export const serializeFollowerIndex = (followerIndex: FollowerIndex): FollowerIndexToEs => ({
remote_cluster: followerIndex.remoteCluster,
leader_index: followerIndex.leaderIndex,
...serializeAdvancedSettings(followerIndex),

View file

@ -3,14 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const arrify = val => (Array.isArray(val) ? val : [val]);
export const arrify = (val: any): any[] => (Array.isArray(val) ? val : [val]);
/**
* Utilty to add some latency in a Promise chain
*
* @param {number} time Time in millisecond to wait
*/
export const wait = (time = 1000) => data => {
export const wait = (time = 1000) => (data: any): Promise<any> => {
return new Promise(resolve => {
setTimeout(() => resolve(data), time);
});
@ -19,8 +19,11 @@ export const wait = (time = 1000) => data => {
/**
* Utility to remove empty fields ("") from a request body
*/
export const removeEmptyFields = body =>
Object.entries(body).reduce((acc, [key, value]) => {
export const removeEmptyFields = (body: Record<string, any>): Record<string, any> =>
Object.entries(body).reduce((acc: Record<string, any>, [key, value]: [string, any]): Record<
string,
any
> => {
if (value !== '') {
acc[key] = value;
}

View file

@ -0,0 +1,186 @@
/*
* 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.
*/
export interface AutoFollowPattern {
name: string;
active: boolean;
remoteCluster: string;
leaderIndexPatterns: string[];
followIndexPattern: string;
}
export interface AutoFollowPatternFromEs {
name: string;
pattern: {
active: boolean;
remote_cluster: string;
leader_index_patterns: string[];
follow_index_pattern: string;
};
}
export interface AutoFollowPatternToEs {
remote_cluster: string;
leader_index_patterns: string[];
follow_index_pattern: string;
}
export interface ShardFromEs {
remote_cluster: string;
leader_index: string;
shard_id: number;
leader_global_checkpoint: number;
leader_max_seq_no: number;
follower_global_checkpoint: number;
follower_max_seq_no: number;
last_requested_seq_no: number;
outstanding_read_requests: number;
outstanding_write_requests: number;
write_buffer_operation_count: number;
write_buffer_size_in_bytes: number;
follower_mapping_version: number;
follower_settings_version: number;
total_read_time_millis: number;
total_read_remote_exec_time_millis: number;
successful_read_requests: number;
failed_read_requests: number;
operations_read: number;
bytes_read: number;
total_write_time_millis: number;
successful_write_requests: number;
failed_write_requests: number;
operations_written: number;
// This is an array of exception objects
read_exceptions: any[];
time_since_last_read_millis: number;
}
export interface Shard {
remoteCluster: string;
leaderIndex: string;
id: number;
leaderGlobalCheckpoint: number;
leaderMaxSequenceNum: number;
followerGlobalCheckpoint: number;
followerMaxSequenceNum: number;
lastRequestedSequenceNum: number;
outstandingReadRequestsCount: number;
outstandingWriteRequestsCount: number;
writeBufferOperationsCount: number;
writeBufferSizeBytes: number;
followerMappingVersion: number;
followerSettingsVersion: number;
totalReadTimeMs: number;
totalReadRemoteExecTimeMs: number;
successfulReadRequestCount: number;
failedReadRequestsCount: number;
operationsReadCount: number;
bytesReadCount: number;
totalWriteTimeMs: number;
successfulWriteRequestsCount: number;
failedWriteRequestsCount: number;
operationsWrittenCount: number;
// This is an array of exception objects
readExceptions: any[];
timeSinceLastReadMs: number;
}
export interface FollowerIndexFromEs {
follower_index: string;
remote_cluster: string;
leader_index: string;
status: string;
// Once https://github.com/elastic/elasticsearch/issues/54996 is resolved so that paused follower
// indices contain this information, we can removed this optional typing as well as the optional
// typing in FollowerIndexAdvancedSettings and FollowerIndexAdvancedSettingsToEs.
parameters?: FollowerIndexAdvancedSettingsToEs;
shards: ShardFromEs[];
}
export interface FollowerIndex extends FollowerIndexAdvancedSettings {
name: string;
remoteCluster: string;
leaderIndex: string;
status: string;
shards: Shard[];
}
export interface FollowerIndexToEs extends FollowerIndexAdvancedSettingsToEs {
remote_cluster: string;
leader_index: string;
}
export interface FollowerIndexAdvancedSettings {
maxReadRequestOperationCount?: number;
maxOutstandingReadRequests?: number;
maxReadRequestSize?: string; // byte value
maxWriteRequestOperationCount?: number;
maxWriteRequestSize?: string; // byte value
maxOutstandingWriteRequests?: number;
maxWriteBufferCount?: number;
maxWriteBufferSize?: string; // byte value
maxRetryDelay?: string; // time value
readPollTimeout?: string; // time value
}
export interface FollowerIndexAdvancedSettingsToEs {
max_read_request_operation_count?: number;
max_outstanding_read_requests?: number;
max_read_request_size?: string; // byte value
max_write_request_operation_count?: number;
max_write_request_size?: string; // byte value
max_outstanding_write_requests?: number;
max_write_buffer_count?: number;
max_write_buffer_size?: string; // byte value
max_retry_delay?: string; // time value
read_poll_timeout?: string; // time value
}
export interface RecentAutoFollowError {
timestamp: number;
leaderIndex: string;
autoFollowException: {
type: string;
reason: string;
};
}
export interface RecentAutoFollowErrorFromEs {
timestamp: number;
leader_index: string;
auto_follow_exception: {
type: string;
reason: string;
};
}
export interface AutoFollowedCluster {
clusterName: string;
timeSinceLastCheckMillis: number;
lastSeenMetadataVersion: number;
}
export interface AutoFollowedClusterFromEs {
cluster_name: string;
time_since_last_check_millis: number;
last_seen_metadata_version: number;
}
export interface AutoFollowStats {
numberOfFailedFollowIndices: number;
numberOfFailedRemoteClusterStateRequests: number;
numberOfSuccessfulFollowIndices: number;
recentAutoFollowErrors: RecentAutoFollowError[];
autoFollowedClusters: AutoFollowedCluster[];
}
export interface AutoFollowStatsFromEs {
number_of_failed_follow_indices: number;
number_of_failed_remote_cluster_state_requests: number;
number_of_successful_follow_indices: number;
recent_auto_follow_errors: RecentAutoFollowErrorFromEs[];
auto_followed_clusters: AutoFollowedClusterFromEs[];
}

View file

@ -0,0 +1,17 @@
{
"id": "crossClusterReplication",
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": [
"home",
"licensing",
"management",
"remoteClusters",
"indexManagement"
],
"optionalPlugins": [
"usageCollection"
],
"configPath": ["xpack", "ccr"]
}

View file

@ -3,11 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import '../../public/np_ready/app/services/breadcrumbs.mock';
import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
import { indexPatterns } from '../../../../../../src/plugins/data/public';
jest.mock('ui/new_platform');
import { indexPatterns } from '../../../../../../src/plugins/data/public';
import './mocks';
import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
const { setup } = pageHelpers.autoFollowPatternAdd;

View file

@ -4,13 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import '../../public/np_ready/app/services/breadcrumbs.mock';
import { AutoFollowPatternForm } from '../../public/np_ready/app/components/auto_follow_pattern_form';
import { AutoFollowPatternForm } from '../../app/components/auto_follow_pattern_form';
import './mocks';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { AUTO_FOLLOW_PATTERN_EDIT } from './helpers/constants';
jest.mock('ui/new_platform');
const { setup } = pageHelpers.autoFollowPatternEdit;
const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd;

View file

@ -4,13 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import '../../public/np_ready/app/services/breadcrumbs.mock';
import { getAutoFollowPatternMock } from './fixtures/auto_follow_pattern';
import './mocks';
import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
import { getAutoFollowPatternClientMock } from '../../fixtures/auto_follow_pattern';
jest.mock('ui/new_platform');
const { setup } = pageHelpers.autoFollowPatternList;
describe('<AutoFollowPatternList />', () => {
@ -79,11 +76,11 @@ describe('<AutoFollowPatternList />', () => {
const testPrefix = 'prefix_';
const testSuffix = '_suffix';
const autoFollowPattern1 = getAutoFollowPatternClientMock({
const autoFollowPattern1 = getAutoFollowPatternMock({
name: `a${getRandomString()}`,
followIndexPattern: `${testPrefix}{{leader_index}}${testSuffix}`,
});
const autoFollowPattern2 = getAutoFollowPatternClientMock({
const autoFollowPattern2 = getAutoFollowPatternMock({
name: `b${getRandomString()}`,
followIndexPattern: '{{leader_index}}', // no prefix nor suffix
});
@ -305,10 +302,12 @@ describe('<AutoFollowPatternList />', () => {
const message = 'bar';
const recentAutoFollowErrors = [
{
timestamp: 1587081600021,
leaderIndex: `${autoFollowPattern1.name}:my-leader-test`,
autoFollowException: { type: 'exception', reason: message },
},
{
timestamp: 1587081600021,
leaderIndex: `${autoFollowPattern2.name}:my-leader-test`,
autoFollowException: { type: 'exception', reason: message },
},
@ -327,7 +326,7 @@ describe('<AutoFollowPatternList />', () => {
expect(exists('autoFollowPatternDetail.errors')).toBe(true);
expect(exists('autoFollowPatternDetail.titleErrors')).toBe(true);
expect(find('autoFollowPatternDetail.recentError').map(error => error.text())).toEqual([
message,
'April 16th, 2020 8:00:00 PM: bar',
]);
});
});

View file

@ -0,0 +1,28 @@
/*
* 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 { getRandomString } from '../../../../../../test_utils';
import { AutoFollowPattern } from '../../../../common/types';
export const getAutoFollowPatternMock = ({
name = getRandomString(),
active = false,
remoteCluster = getRandomString(),
leaderIndexPatterns = [`${getRandomString()}-*`],
followIndexPattern = getRandomString(),
}: {
name: string;
active: boolean;
remoteCluster: string;
leaderIndexPatterns: string[];
followIndexPattern: string;
}): AutoFollowPattern => ({
name,
active,
remoteCluster,
leaderIndexPatterns,
followIndexPattern,
});

View file

@ -0,0 +1,70 @@
/*
* 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 { getRandomString } from '../../../../../../test_utils';
import { FollowerIndex } from '../../../../common/types';
const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires
const chance = new Chance();
interface FollowerIndexMock {
name: string;
remoteCluster: string;
leaderIndex: string;
status: string;
}
export const getFollowerIndexMock = ({
name = getRandomString(),
remoteCluster = getRandomString(),
leaderIndex = getRandomString(),
status = 'Active',
}: FollowerIndexMock): FollowerIndex => ({
name,
remoteCluster,
leaderIndex,
status,
maxReadRequestOperationCount: chance.integer(),
maxOutstandingReadRequests: chance.integer(),
maxReadRequestSize: getRandomString({ length: 5 }),
maxWriteRequestOperationCount: chance.integer(),
maxWriteRequestSize: '9223372036854775807b',
maxOutstandingWriteRequests: chance.integer(),
maxWriteBufferCount: chance.integer(),
maxWriteBufferSize: getRandomString({ length: 5 }),
maxRetryDelay: getRandomString({ length: 5 }),
readPollTimeout: getRandomString({ length: 5 }),
shards: [
{
id: 0,
remoteCluster,
leaderIndex,
leaderGlobalCheckpoint: chance.integer(),
leaderMaxSequenceNum: chance.integer(),
followerGlobalCheckpoint: chance.integer(),
followerMaxSequenceNum: chance.integer(),
lastRequestedSequenceNum: chance.integer(),
outstandingReadRequestsCount: chance.integer(),
outstandingWriteRequestsCount: chance.integer(),
writeBufferOperationsCount: chance.integer(),
writeBufferSizeBytes: chance.integer(),
followerMappingVersion: chance.integer(),
followerSettingsVersion: chance.integer(),
totalReadTimeMs: chance.integer(),
totalReadRemoteExecTimeMs: chance.integer(),
successfulReadRequestCount: chance.integer(),
failedReadRequestsCount: chance.integer(),
operationsReadCount: chance.integer(),
bytesReadCount: chance.integer(),
totalWriteTimeMs: chance.integer(),
successfulWriteRequestsCount: chance.integer(),
failedWriteRequestsCount: chance.integer(),
operationsWrittenCount: chance.integer(),
readExceptions: [],
timeSinceLastReadMs: chance.integer(),
},
],
});

View file

@ -4,13 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import '../../public/np_ready/app/services/breadcrumbs.mock';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { RemoteClustersFormField } from '../../public/np_ready/app/components';
import { indexPatterns } from '../../../../../../src/plugins/data/public';
jest.mock('ui/new_platform');
import './mocks';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { RemoteClustersFormField } from '../../app/components';
const { setup } = pageHelpers.followerIndexAdd;
const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd;

View file

@ -4,12 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import '../../public/np_ready/app/services/breadcrumbs.mock';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { FollowerIndexForm } from '../../public/np_ready/app/components/follower_index_form/follower_index_form';
import { FollowerIndexForm } from '../../app/components/follower_index_form/follower_index_form';
import './mocks';
import { FOLLOWER_INDEX_EDIT } from './helpers/constants';
jest.mock('ui/new_platform');
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
const { setup } = pageHelpers.followerIndexEdit;
const { setup: setupFollowerIndexAdd } = pageHelpers.followerIndexAdd;

View file

@ -4,12 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getFollowerIndexMock } from './fixtures/follower_index';
import './mocks';
import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
import { getFollowerIndexMock } from '../../fixtures/follower_index';
jest.mock('ui/new_platform');
const { setup } = pageHelpers.followerIndexList;
describe('<FollowerIndicesList />', () => {

View file

@ -5,9 +5,9 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
import { AutoFollowPatternAdd } from '../../../public/np_ready/app/sections/auto_follow_pattern_add';
import { ccrStore } from '../../../public/np_ready/app/store';
import routing from '../../../public/np_ready/app/services/routing';
import { AutoFollowPatternAdd } from '../../../app/sections/auto_follow_pattern_add';
import { ccrStore } from '../../../app/store';
import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,

View file

@ -5,9 +5,9 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
import { AutoFollowPatternEdit } from '../../../public/np_ready/app/sections/auto_follow_pattern_edit';
import { ccrStore } from '../../../public/np_ready/app/store';
import routing from '../../../public/np_ready/app/services/routing';
import { AutoFollowPatternEdit } from '../../../app/sections/auto_follow_pattern_edit';
import { ccrStore } from '../../../app/store';
import { routing } from '../../../app/services/routing';
import { AUTO_FOLLOW_PATTERN_EDIT_NAME } from './constants';

View file

@ -5,9 +5,9 @@
*/
import { registerTestBed, findTestSubject } from '../../../../../../test_utils';
import { AutoFollowPatternList } from '../../../public/np_ready/app/sections/home/auto_follow_pattern_list';
import { ccrStore } from '../../../public/np_ready/app/store';
import routing from '../../../public/np_ready/app/services/routing';
import { AutoFollowPatternList } from '../../../app/sections/home/auto_follow_pattern_list';
import { ccrStore } from '../../../app/store';
import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,

View file

@ -5,9 +5,9 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
import { FollowerIndexAdd } from '../../../public/np_ready/app/sections/follower_index_add';
import { ccrStore } from '../../../public/np_ready/app/store';
import routing from '../../../public/np_ready/app/services/routing';
import { FollowerIndexAdd } from '../../../app/sections/follower_index_add';
import { ccrStore } from '../../../app/store';
import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,

View file

@ -5,9 +5,9 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
import { FollowerIndexEdit } from '../../../public/np_ready/app/sections/follower_index_edit';
import { ccrStore } from '../../../public/np_ready/app/store';
import routing from '../../../public/np_ready/app/services/routing';
import { FollowerIndexEdit } from '../../../app/sections/follower_index_edit';
import { ccrStore } from '../../../app/store';
import { routing } from '../../../app/services/routing';
import { FOLLOWER_INDEX_EDIT_NAME } from './constants';

View file

@ -5,9 +5,9 @@
*/
import { registerTestBed, findTestSubject } from '../../../../../../test_utils';
import { FollowerIndicesList } from '../../../public/np_ready/app/sections/home/follower_indices_list';
import { ccrStore } from '../../../public/np_ready/app/store';
import routing from '../../../public/np_ready/app/services/routing';
import { FollowerIndicesList } from '../../../app/sections/home/follower_indices_list';
import { ccrStore } from '../../../app/store';
import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,

View file

@ -5,10 +5,10 @@
*/
import { registerTestBed } from '../../../../../../test_utils';
import { CrossClusterReplicationHome } from '../../../public/np_ready/app/sections/home/home';
import { ccrStore } from '../../../public/np_ready/app/store';
import routing from '../../../public/np_ready/app/services/routing';
import { BASE_PATH } from '../../../common/constants';
import { BASE_PATH } from '../../../../common/constants';
import { CrossClusterReplicationHome } from '../../../app/sections/home/home';
import { ccrStore } from '../../../app/store';
import { routing } from '../../../app/services/routing';
const testBedConfig = {
store: ccrStore,

View file

@ -7,7 +7,7 @@
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import { setHttpClient } from '../../../public/np_ready/app/services/api';
import { setHttpClient } from '../../../app/services/api';
import { init as initHttpRequests } from './http_requests';
export const setupEnvironment = () => {

View file

@ -4,11 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import '../../public/np_ready/app/services/breadcrumbs.mock';
import './mocks';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
jest.mock('ui/new_platform');
const { setup } = pageHelpers.home;
describe('<CrossClusterReplicationHome />', () => {
@ -36,7 +34,7 @@ describe('<CrossClusterReplicationHome />', () => {
({ exists, find, component } = setup());
});
test('should set the correct an app title', () => {
test('should set the correct app title', () => {
expect(exists('appTitle')).toBe(true);
expect(find('appTitle').text()).toEqual('Cross-Cluster Replication');
});

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('./breadcrumbs', () => ({
...jest.requireActual('./breadcrumbs'),
jest.mock('../../../app/services/breadcrumbs', () => ({
...jest.requireActual('../../../app/services/breadcrumbs'),
setBreadcrumbs: jest.fn(),
}));

View file

@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { isEsErrorFactory } from './is_es_error_factory';
import './breadcrumbs.mock';
import './track_ui_metric.mock';

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;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('../../../app/services/track_ui_metric', () => ({
...jest.requireActual('../../../app/services/track_ui_metric'),
trackUiMetric: jest.fn(),
trackUserRequest: (request: Promise<any>) => {
return request.then(response => response);
},
}));

View file

@ -5,8 +5,8 @@
*/
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Route, Switch, Redirect, withRouter } from 'react-router-dom';
import { Route, Switch, Redirect, withRouter, RouteComponentProps } from 'react-router-dom';
import { History } from 'history';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@ -20,12 +20,14 @@ import {
EuiTitle,
} from '@elastic/eui';
import { BASE_PATH } from '../../../common/constants';
import { BASE_PATH } from '../../common/constants';
import { getFatalErrors } from './services/notifications';
import { SectionError } from './components';
import routing from './services/routing';
import { routing } from './services/routing';
// @ts-ignore
import { loadPermissions } from './services/api';
// @ts-ignore
import {
CrossClusterReplicationHome,
AutoFollowPatternAdd,
@ -34,16 +36,21 @@ import {
FollowerIndexEdit,
} from './sections';
class AppComponent extends Component {
static propTypes = {
history: PropTypes.shape({
push: PropTypes.func.isRequired,
createHref: PropTypes.func.isRequired,
}).isRequired,
};
interface AppProps {
history: History;
location: any;
}
constructor(...args) {
super(...args);
interface AppState {
isFetchingPermissions: boolean;
fetchPermissionError: any;
hasPermission: boolean;
missingClusterPrivileges: any[];
}
class AppComponent extends Component<RouteComponentProps & AppProps, AppState> {
constructor(props: any) {
super(props);
this.registerRouter();
this.state = {
@ -54,18 +61,10 @@ class AppComponent extends Component {
};
}
UNSAFE_componentWillMount() {
routing.userHasLeftApp = false;
}
componentDidMount() {
this.checkPermissions();
}
componentWillUnmount() {
routing.userHasLeftApp = true;
}
async checkPermissions() {
this.setState({
isFetchingPermissions: true,
@ -163,7 +162,6 @@ class AppComponent extends Component {
<EuiPageContent horizontalPosition="center">
<EuiEmptyPrompt
iconType="securityApp"
iconColor={null}
title={
<h2>
<FormattedMessage

View file

@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import { AutoFollowPatternDeleteProvider } from '../auto_follow_pattern_delete_provider';
// @ts-ignore
import routing from '../../services/routing';
import { routing } from '../../services/routing';
const actionsAriaLabel = i18n.translate(
'xpack.crossClusterReplication.autoFollowActionMenu.autoFollowPatternActionMenuButtonAriaLabel',

View file

@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { deleteAutoFollowPattern } from '../store/actions';
import { arrify } from '../../../../common/services/utils';
import { arrify } from '../../../common/services/utils';
class AutoFollowPatternDeleteProviderUi extends PureComponent {
state = {

View file

@ -29,10 +29,10 @@ import {
EuiTitle,
} from '@elastic/eui';
import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public';
import { indexPatterns } from '../../../../../../../../src/plugins/data/public';
import { indices } from '../../../../../../src/plugins/es_ui_shared/public';
import { indexPatterns } from '../../../../../../src/plugins/data/public';
import routing from '../services/routing';
import { routing } from '../services/routing';
import { extractQueryParams } from '../services/query_params';
import { getRemoteClusterName } from '../services/get_remote_cluster_name';
import { API_STATUS } from '../constants';
@ -512,7 +512,6 @@ export class AutoFollowPatternForm extends PureComponent {
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFormRow
className="ccrFollowerIndicesFormRow"
label={
<FormattedMessage
id="xpack.crossClusterReplication.autoFollowPatternForm.autoFollowPattern.fieldPrefixLabel"
@ -535,7 +534,6 @@ export class AutoFollowPatternForm extends PureComponent {
<EuiFlexItem>
<EuiFormRow
className="ccrFollowerIndicesFormRow"
label={
<FormattedMessage
id="xpack.crossClusterReplication.autoFollowPatternForm.autoFollowPattern.fieldSuffixLabel"
@ -557,9 +555,7 @@ export class AutoFollowPatternForm extends PureComponent {
</EuiFlexItem>
</EuiFlexGroup>
<EuiFormHelpText
className={isPrefixInvalid || isSuffixInvalid ? null : 'ccrFollowerIndicesHelpText'}
>
<EuiFormHelpText>
<FormattedMessage
id="xpack.crossClusterReplication.autoFollowPatternForm.fieldFollowerIndicesHelpLabel"
defaultMessage="Spaces and the characters {characterList} are not allowed."

View file

@ -26,7 +26,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization';
import { serializeAutoFollowPattern } from '../../../common/services/auto_follow_pattern_serialization';
export class AutoFollowPatternRequestFlyout extends PureComponent {
static propTypes = {

View file

@ -28,9 +28,9 @@ import {
EuiTitle,
} from '@elastic/eui';
import { indices } from '../../../../../../../../../src/plugins/es_ui_shared/public';
import { indices } from '../../../../../../../src/plugins/es_ui_shared/public';
import { indexNameValidator, leaderIndexValidator } from '../../services/input_validation';
import routing from '../../services/routing';
import { routing } from '../../services/routing';
import { getFatalErrors } from '../../services/notifications';
import { loadIndices } from '../../services/api';
import { API_STATUS } from '../../constants';

View file

@ -26,7 +26,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import { serializeFollowerIndex } from '../../../../../common/services/follower_index_serialization';
import { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization';
export class FollowerIndexRequestFlyout extends PureComponent {
static propTypes = {

View file

@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { pauseFollowerIndex } from '../store/actions';
import { arrify } from '../../../../common/services/utils';
import { arrify } from '../../../common/services/utils';
import { areAllSettingsDefault } from '../services/follower_index_default_settings';
class FollowerIndexPauseProviderUi extends PureComponent {

View file

@ -11,9 +11,9 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiConfirmModal, EuiLink, EuiOverlayMask } from '@elastic/eui';
import routing from '../services/routing';
import { routing } from '../services/routing';
import { resumeFollowerIndex } from '../store/actions';
import { arrify } from '../../../../common/services/utils';
import { arrify } from '../../../common/services/utils';
class FollowerIndexResumeProviderUi extends PureComponent {
static propTypes = {

Some files were not shown because too many files have changed in this diff Show more