[Upgrade Assistant] Add warning when remote clusters are configured (#125138) (#125296)

This commit is contained in:
Alison Goryachev 2022-02-11 08:04:50 -05:00 committed by GitHub
parent 9cba32e630
commit f7f0e7095a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 273 additions and 7 deletions

View file

@ -6,7 +6,7 @@
"name": "Stack Management",
"githubTeam": "kibana-stack-management"
},
"requiredPlugins": ["licensing", "management", "indexManagement", "features"],
"requiredPlugins": ["licensing", "management", "indexManagement", "features", "share"],
"optionalPlugins": ["usageCollection", "cloud"],
"server": true,
"ui": true,

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { SerializableRecord } from '@kbn/utility-types';
import { ManagementAppLocator } from 'src/plugins/management/common';
import { LocatorDefinition } from '../../../../src/plugins/share/public/';
export const REMOTE_CLUSTERS_LOCATOR_ID = 'REMOTE_CLUSTERS_LOCATOR';
export interface RemoteClustersLocatorParams extends SerializableRecord {
page: 'remoteClusters';
}
export interface RemoteClustersLocatorDefinitionDependencies {
managementAppLocator: ManagementAppLocator;
}
export class RemoteClustersLocatorDefinition
implements LocatorDefinition<RemoteClustersLocatorParams>
{
constructor(protected readonly deps: RemoteClustersLocatorDefinitionDependencies) {}
public readonly id = REMOTE_CLUSTERS_LOCATOR_ID;
public readonly getLocation = async (params: RemoteClustersLocatorParams) => {
const location = await this.deps.managementAppLocator.getLocation({
sectionId: 'data',
appId: 'remote_clusters',
});
switch (params.page) {
case 'remoteClusters': {
return {
...location,
path: location.path,
};
}
}
};
}

View file

@ -16,6 +16,7 @@ import { init as initUiMetric } from './application/services/ui_metric';
import { init as initNotification } from './application/services/notification';
import { init as initRedirect } from './application/services/redirect';
import { Dependencies, ClientConfigType } from './types';
import { RemoteClustersLocatorDefinition } from './locator';
export interface RemoteClustersPluginSetup {
isUiEnabled: boolean;
@ -28,7 +29,7 @@ export class RemoteClustersUIPlugin
setup(
{ notifications: { toasts }, http, getStartServices }: CoreSetup,
{ management, usageCollection, cloud }: Dependencies
{ management, usageCollection, cloud, share }: Dependencies
) {
const {
ui: { enabled: isRemoteClustersUiEnabled },
@ -79,6 +80,12 @@ export class RemoteClustersUIPlugin
};
},
});
share.url.locators.create(
new RemoteClustersLocatorDefinition({
managementAppLocator: management.locator,
})
);
}
return {

View file

@ -8,6 +8,7 @@
import { ManagementSetup } from 'src/plugins/management/public';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
import { RegisterManagementAppArgs } from 'src/plugins/management/public';
import { SharePluginSetup } from 'src/plugins/share/public';
import { I18nStart } from 'kibana/public';
import { CloudSetup } from '../../cloud/public';
@ -15,6 +16,7 @@ export interface Dependencies {
management: ManagementSetup;
usageCollection: UsageCollectionSetup;
cloud: CloudSetup;
share: SharePluginSetup;
}
export interface ClientConfigType {

View file

@ -44,6 +44,7 @@ describe('ES deprecations table', () => {
aliases: [],
},
});
httpRequestsMockHelpers.setLoadRemoteClustersResponse([]);
await act(async () => {
testBed = await setupElasticsearchPage({ isReadOnlyMode: false });
@ -105,6 +106,25 @@ describe('ES deprecations table', () => {
expect(find('warningDeprecationsCount').text()).toContain(warningDeprecations.length);
});
describe('remote clusters callout', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadRemoteClustersResponse(['test_remote_cluster']);
await act(async () => {
testBed = await setupElasticsearchPage({ isReadOnlyMode: false });
});
testBed.component.update();
});
it('shows a warning message if a user has remote clusters configured', () => {
const { exists } = testBed;
// Verify warning exists
expect(exists('remoteClustersWarningCallout')).toBe(true);
});
});
describe('search bar', () => {
it('filters results by "critical" status', async () => {
const { find, actions } = testBed;

View file

@ -203,6 +203,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
const setLoadRemoteClustersResponse = (response?: object, error?: ResponseError) => {
const status = error ? error.statusCode || 400 : 200;
const body = error ? error : response;
server.respondWith('GET', `${API_BASE_PATH}/remote_clusters`, [
status,
{ 'Content-Type': 'application/json' },
JSON.stringify(body),
]);
};
return {
setLoadCloudBackupStatusResponse,
setLoadEsDeprecationsResponse,
@ -220,6 +231,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
setReindexStatusResponse,
setLoadMlUpgradeModeResponse,
setGetUpgradeStatusResponse,
setLoadRemoteClustersResponse,
};
};

View file

@ -8,7 +8,7 @@
import React, { useEffect, useMemo } from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { EuiPageHeader, EuiSpacer, EuiPageContent, EuiLink } from '@elastic/eui';
import { EuiPageHeader, EuiSpacer, EuiPageContent, EuiLink, EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { DocLinksStart } from 'kibana/public';
@ -51,6 +51,26 @@ const i18nTexts = {
isLoading: i18n.translate('xpack.upgradeAssistant.esDeprecations.loadingText', {
defaultMessage: 'Loading deprecation issues…',
}),
remoteClustersDetectedTitle: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.remoteClustersDetectedTitle',
{
defaultMessage: 'Remote cluster compatibility',
}
),
getRemoteClustersDetectedDescription: (remoteClustersCount: number) =>
i18n.translate('xpack.upgradeAssistant.esDeprecations.remoteClustersDetectedDescription', {
defaultMessage:
'You have {remoteClustersCount} {remoteClustersCount, plural, one {remote cluster} other {remote clusters}} configured. If you use cross-cluster search, note that 8.x can only search remote clusters running the previous minor version or later. If you use cross-cluster replication, a cluster that contains follower indices must run the same or newer version as the remote cluster.',
values: {
remoteClustersCount,
},
}),
remoteClustersLinkText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.remoteClustersLinkText',
{
defaultMessage: 'View remote clusters.',
}
),
};
const getBatchReindexLink = (docLinks: DocLinksStart) => {
@ -75,6 +95,22 @@ const getBatchReindexLink = (docLinks: DocLinksStart) => {
);
};
const RemoteClustersAppLink: React.FunctionComponent = () => {
const {
plugins: { share },
} = useAppContext();
const remoteClustersUrl = share.url.locators
.get('REMOTE_CLUSTERS_LOCATOR')
?.useUrl({ page: 'remoteClusters' });
return (
<EuiLink href={remoteClustersUrl} data-test-subj="remoteClustersLink">
{i18nTexts.remoteClustersLinkText}
</EuiLink>
);
};
export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => {
const {
services: {
@ -85,6 +121,7 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => {
} = useAppContext();
const { data: esDeprecations, isLoading, error, resendRequest } = api.useLoadEsDeprecations();
const { data: remoteClusters } = api.useLoadRemoteClusters();
const deprecationsCountByLevel: {
warningDeprecations: number;
@ -140,10 +177,29 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => {
</>
}
>
<DeprecationCount
totalCriticalDeprecations={deprecationsCountByLevel.criticalDeprecations}
totalWarningDeprecations={deprecationsCountByLevel.warningDeprecations}
/>
<>
{remoteClusters && remoteClusters.length > 0 && (
<>
<EuiCallOut
title={i18nTexts.remoteClustersDetectedTitle}
color="warning"
iconType="help"
data-test-subj="remoteClustersWarningCallout"
>
<p>
{i18nTexts.getRemoteClustersDetectedDescription(remoteClusters.length)}{' '}
<RemoteClustersAppLink />
</p>
</EuiCallOut>
<EuiSpacer />
</>
)}
<DeprecationCount
totalCriticalDeprecations={deprecationsCountByLevel.criticalDeprecations}
totalWarningDeprecations={deprecationsCountByLevel.warningDeprecations}
/>
</>
</EuiPageHeader>
<EuiSpacer size="l" />

View file

@ -239,6 +239,13 @@ export class ApiService {
method: 'get',
});
}
public useLoadRemoteClusters() {
return this.useRequest<string[]>({
path: `${API_BASE_PATH}/remote_clusters`,
method: 'get',
});
}
}
export const apiService = new ApiService();

View file

@ -18,6 +18,7 @@ import { registerUpdateSettingsRoute } from './update_index_settings';
import { registerMlSnapshotRoutes } from './ml_snapshots';
import { ReindexWorker } from '../lib/reindexing';
import { registerUpgradeStatusRoute } from './status';
import { registerRemoteClustersRoute } from './remote_clusters';
export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) {
registerAppRoutes(dependencies);
@ -32,4 +33,5 @@ export function registerRoutes(dependencies: RouteDependencies, getWorker: () =>
registerMlSnapshotRoutes(dependencies);
// Route for cloud to retrieve the upgrade status for ES and Kibana
registerUpgradeStatusRoute(dependencies);
registerRemoteClustersRoute(dependencies);
}

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { API_BASE_PATH } from '../../common/constants';
import { versionCheckHandlerWrapper } from '../lib/es_version_precheck';
import { RouteDependencies } from '../types';
export function registerRemoteClustersRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.get(
{
path: `${API_BASE_PATH}/remote_clusters`,
validate: false,
},
versionCheckHandlerWrapper(
async (
{
core: {
elasticsearch: { client },
},
},
request,
response
) => {
try {
const { body: clustersByName } = await client.asCurrentUser.cluster.remoteInfo();
const remoteClusters = Object.keys(clustersByName);
return response.ok({ body: remoteClusters });
} catch (error) {
return handleEsError({ error, response });
}
}
)
);
}

View file

@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./privileges'));
loadTestFile(require.resolve('./es_deprecations'));
loadTestFile(require.resolve('./es_deprecation_logs'));
loadTestFile(require.resolve('./remote_clusters'));
});
}

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { API_BASE_PATH } from '../../../../plugins/upgrade_assistant/common/constants';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const es = getService('es');
const log = getService('log');
describe('Remote clusters', () => {
describe('GET /api/upgrade_assistant/remote_clusters', () => {
before(async () => {
try {
// Configure a remote cluster
await es.cluster.putSettings({
body: {
persistent: {
cluster: {
remote: {
test_cluster: {
seeds: ['127.0.0.1:9400'],
},
},
},
},
},
});
} catch (e) {
log.debug('Error creating remote cluster');
throw e;
}
});
after(async () => {
try {
// Delete remote cluster
await es.cluster.putSettings({
body: {
persistent: {
cluster: {
remote: {
test_cluster: {
seeds: null,
},
},
},
},
},
});
} catch (e) {
log.debug('Error deleting remote cluster');
throw e;
}
});
it('returns an array of remote clusters', async () => {
const { body: apiRequestResponse } = await supertest
.get(`${API_BASE_PATH}/remote_clusters`)
.set('kbn-xsrf', 'xxx')
.expect(200);
expect(Array.isArray(apiRequestResponse)).be(true);
expect(apiRequestResponse.length).be(1);
});
});
});
}