Show interstitial prompt when ES is upgrading (#31309)

* Show interstitial prompt when ES is upgrading

* Update copy
This commit is contained in:
Josh Dover 2019-02-21 14:05:16 -06:00 committed by GitHub
parent 9458387333
commit 46edc4fe8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 226 additions and 12 deletions

View file

@ -4,12 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SemVer } from 'semver';
import pkg from '../../../package.json';
// Extract version information
const currentVersionNum = pkg.version as string;
const matches = currentVersionNum.match(/^([1-9]+)\.([0-9]+)\.([0-9]+)$/)!;
export const CURRENT_MAJOR_VERSION = matches[1];
export const NEXT_MAJOR_VERSION = (parseInt(CURRENT_MAJOR_VERSION, 10) + 1).toString();
export const PREV_MAJOR_VERSION = (parseInt(CURRENT_MAJOR_VERSION, 10) - 1).toString();
export const CURRENT_VERSION = new SemVer(pkg.version as string);
export const CURRENT_MAJOR_VERSION = CURRENT_VERSION.major;
export const NEXT_MAJOR_VERSION = CURRENT_VERSION.major + 1;
export const PREV_MAJOR_VERSION = CURRENT_VERSION.major - 1;

View file

@ -59,4 +59,14 @@ describe('UpgradeAssistantTabs', () => {
// Should pass down error status to child component
expect(wrapper.find(OverviewTab).prop('loadingState')).toEqual(LoadingState.Error);
});
it('upgrade error', async () => {
// @ts-ignore
axios.get.mockRejectedValue({ response: { status: 426 } });
const wrapper = mountWithIntl(<UpgradeAssistantTabs />);
await promisesToResolve();
wrapper.update();
// Should display an informative message if the cluster is currently mid-upgrade
expect(wrapper.find('EuiEmptyPrompt').exists()).toBe(true);
});
});

View file

@ -5,11 +5,17 @@
*/
import axios from 'axios';
import { findIndex, set } from 'lodash';
import { findIndex, get, set } from 'lodash';
import React from 'react';
import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import {
EuiEmptyPrompt,
EuiPageContent,
EuiPageContentBody,
EuiTabbedContent,
EuiTabbedContentTab,
} from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import chrome from 'ui/chrome';
import { kfetch } from 'ui/kfetch';
@ -26,6 +32,7 @@ interface TabsState {
checkupData?: UpgradeAssistantStatus;
selectedTabIndex: number;
telemetryState: TelemetryState;
upgradeableCluster: boolean;
}
export class UpgradeAssistantTabsUI extends React.Component<
@ -37,6 +44,7 @@ export class UpgradeAssistantTabsUI extends React.Component<
this.state = {
loadingState: LoadingState.Loading,
upgradeableCluster: true,
selectedTabIndex: 0,
telemetryState: TelemetryState.Complete,
};
@ -50,9 +58,38 @@ export class UpgradeAssistantTabsUI extends React.Component<
}
public render() {
const { selectedTabIndex, telemetryState } = this.state;
const { selectedTabIndex, telemetryState, upgradeableCluster } = this.state;
const tabs = this.tabs;
if (!upgradeableCluster) {
return (
<EuiPageContent>
<EuiPageContentBody>
<EuiEmptyPrompt
iconType="logoElasticsearch"
title={
<h2>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingTitle"
defaultMessage="Your cluster is upgrading"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingDescription"
defaultMessage="One or more Elasticsearch nodes have a newer version of
Elasticsearch than Kibana. Once all your nodes are upgraded, install the latest version of Kibana."
/>
</p>
}
/>
</EuiPageContentBody>
</EuiPageContent>
);
}
return (
<EuiTabbedContent
data-test-subj={
@ -94,7 +131,14 @@ export class UpgradeAssistantTabsUI extends React.Component<
checkupData: resp.data,
});
} catch (e) {
this.setState({ loadingState: LoadingState.Error, loadingError: e });
if (get(e, 'response.status') === 426) {
this.setState({
loadingState: LoadingState.Success,
upgradeableCluster: false,
});
} else {
this.setState({ loadingState: LoadingState.Error, loadingError: e });
}
}
};

View file

@ -0,0 +1,11 @@
/*
* 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 mockedEsVersionPrecheckMethod = jest.fn().mockResolvedValue(true);
export const EsVersionPrecheck = {
assign: 'esVersionCheck',
method: mockedEsVersionPrecheckMethod,
};

View file

@ -0,0 +1,54 @@
/*
* 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 { SemVer } from 'semver';
import { CURRENT_VERSION } from '../../common/version';
import { getAllNodeVersions, verifyAllMatchKibanaVersion } from './es_version_precheck';
describe('getAllNodeVersions', () => {
it('returns a list of unique node versions', async () => {
const callCluster = jest.fn().mockResolvedValue({
nodes: {
node1: { version: '7.0.0' },
node2: { version: '7.0.0' },
node3: { version: '6.0.0' },
},
});
await expect(getAllNodeVersions(callCluster)).resolves.toEqual([
new SemVer('6.0.0'),
new SemVer('7.0.0'),
]);
});
});
describe('verifyAllMatchKibanaVersion', () => {
it('throws if any are higher version', () => {
expect(() =>
verifyAllMatchKibanaVersion([new SemVer('99999.0.0')])
).toThrowErrorMatchingInlineSnapshot(
`"There are some nodes running a different version of Elasticsearch"`
);
});
it('throws if any are lower version', () => {
expect(() =>
verifyAllMatchKibanaVersion([new SemVer('0.0.0')])
).toThrowErrorMatchingInlineSnapshot(
`"There are some nodes running a different version of Elasticsearch"`
);
});
it('does not throw if all are same major', () => {
const versions = [
CURRENT_VERSION,
CURRENT_VERSION.inc('minor'),
CURRENT_VERSION.inc('minor').inc('minor'),
];
expect(() => verifyAllMatchKibanaVersion(versions)).not.toThrow();
});
});

View file

@ -0,0 +1,58 @@
/*
* 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 Boom from 'boom';
import { Request, RouteOptionsPreObject } from 'hapi';
import { uniq } from 'lodash';
import { SemVer } from 'semver';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { CURRENT_VERSION } from '../../common/version';
/**
* Returns an array of all the unique Elasticsearch Node Versions in the Elasticsearch cluster.
* @param request
*/
export const getAllNodeVersions = async (callCluster: CallCluster) => {
// Get the version information for all nodes in the cluster.
const { nodes } = (await callCluster('nodes.info', {
filterPath: 'nodes.*.version',
})) as { nodes: { [nodeId: string]: { version: string } } };
const versionStrings = Object.values(nodes).map(({ version }) => version);
return uniq(versionStrings)
.sort()
.map(version => new SemVer(version));
};
export const verifyAllMatchKibanaVersion = (allNodeVersions: SemVer[]) => {
// Determine if all nodes in the cluster are running the same major version as Kibana.
const anyDifferentEsNodes = !!allNodeVersions.find(
esNodeVersion => esNodeVersion.major !== CURRENT_VERSION.major
);
if (anyDifferentEsNodes) {
throw new Boom(`There are some nodes running a different version of Elasticsearch`, {
// 426 means "Upgrade Required" and is used when semver compatibility is not met.
statusCode: 426,
});
}
};
export const EsVersionPrecheck = {
assign: 'esVersionCheck',
async method(request: Request) {
const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('admin');
const callCluster = callWithRequest.bind(callWithRequest, request) as CallCluster;
const allNodeVersions = await getAllNodeVersions(callCluster);
// This will throw if there is an issue
verifyAllMatchKibanaVersion(allNodeVersions);
return true;
},
} as RouteOptionsPreObject;

View file

@ -4,7 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { Server } from 'hapi';
jest.mock('../lib/es_version_precheck');
import { EsVersionPrecheck } from '../lib/es_version_precheck';
import { registerClusterCheckupRoutes } from './cluster_checkup';
// Need to require to get mock on named export to work.
@ -71,5 +75,18 @@ describe('cluster checkup API', () => {
expect(resp.statusCode).toEqual(500);
});
it('returns a 426 if EsVersionCheck throws', async () => {
(EsVersionPrecheck.method as jest.Mock).mockRejectedValue(
new Boom(`blah`, { statusCode: 426 })
);
const resp = await server.inject({
method: 'GET',
url: '/api/upgrade_assistant/status',
});
expect(resp.statusCode).toEqual(426);
});
});
});

View file

@ -8,6 +8,7 @@ import Boom from 'boom';
import { Legacy } from 'kibana';
import { getUpgradeAssistantStatus } from '../lib/es_migration_apis';
import { EsVersionPrecheck } from '../lib/es_version_precheck';
export function registerClusterCheckupRoutes(server: Legacy.Server) {
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
@ -16,6 +17,9 @@ export function registerClusterCheckupRoutes(server: Legacy.Server) {
server.route({
path: '/api/upgrade_assistant/status',
method: 'GET',
options: {
pre: [EsVersionPrecheck],
},
async handler(request) {
try {
return await getUpgradeAssistantStatus(callWithRequest, request, isCloudEnabled);

View file

@ -5,6 +5,8 @@
*/
import { Server } from 'hapi';
jest.mock('../lib/es_version_precheck');
import { registerDeprecationLoggingRoutes } from './deprecation_logging';
/**

View file

@ -12,6 +12,7 @@ import {
getDeprecationLoggingStatus,
setDeprecationLogging,
} from '../lib/es_deprecation_logging_apis';
import { EsVersionPrecheck } from '../lib/es_version_precheck';
export function registerDeprecationLoggingRoutes(server: Legacy.Server) {
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
@ -19,6 +20,9 @@ export function registerDeprecationLoggingRoutes(server: Legacy.Server) {
server.route({
path: '/api/upgrade_assistant/deprecation_logging',
method: 'GET',
options: {
pre: [EsVersionPrecheck],
},
async handler(request) {
try {
return await getDeprecationLoggingStatus(callWithRequest, request);
@ -32,6 +36,7 @@ export function registerDeprecationLoggingRoutes(server: Legacy.Server) {
path: '/api/upgrade_assistant/deprecation_logging',
method: 'PUT',
options: {
pre: [EsVersionPrecheck],
validate: {
payload: Joi.object({
isEnabled: Joi.boolean(),

View file

@ -18,6 +18,7 @@ const mockReindexService = {
cancelReindexing: jest.fn(),
};
jest.mock('../lib/es_version_precheck');
jest.mock('../lib/reindexing', () => {
return {
reindexServiceFactory: () => mockReindexService,

View file

@ -10,6 +10,7 @@ import { Server } from 'hapi';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { SavedObjectsClient } from 'src/legacy/server/saved_objects';
import { ReindexStatus } from '../../common/types';
import { EsVersionPrecheck } from '../lib/es_version_precheck';
import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing';
import { CredentialStore } from '../lib/reindexing/credential_store';
import { reindexActionsFactory } from '../lib/reindexing/reindex_actions';
@ -65,6 +66,9 @@ export function registerReindexIndicesRoutes(
server.route({
path: `${BASE_PATH}/{indexName}`,
method: 'POST',
options: {
pre: [EsVersionPrecheck],
},
async handler(request) {
const client = request.getSavedObjectsClient();
const { indexName } = request.params;
@ -106,6 +110,9 @@ export function registerReindexIndicesRoutes(
server.route({
path: `${BASE_PATH}/{indexName}`,
method: 'GET',
options: {
pre: [EsVersionPrecheck],
},
async handler(request) {
const client = request.getSavedObjectsClient();
const { indexName } = request.params;
@ -142,6 +149,9 @@ export function registerReindexIndicesRoutes(
server.route({
path: `${BASE_PATH}/{indexName}/cancel`,
method: 'POST',
options: {
pre: [EsVersionPrecheck],
},
async handler(request) {
const client = request.getSavedObjectsClient();
const { indexName } = request.params;